Преглед изворни кода

Merge branch 'main' into dev_sun

elksmmx пре 11 часа
родитељ
комит
513e114fb8
100 измењених фајлова са 9713 додато и 3728 уклоњено
  1. 0 0
      .browser_use_files/browseruse_agent_data/todo.md
  2. 5 1
      .claude/settings.local.json
  3. 4 5
      .env.template
  4. 6 1
      .gitignore
  5. 262 75
      README.md
  6. 167 0
      agent/README.md
  7. 7 15
      agent/__init__.py
  8. 11 0
      agent/cli/__init__.py
  9. 274 0
      agent/cli/interactive.py
  10. 2 3
      agent/core/__init__.py
  11. 58 0
      agent/core/prompts/__init__.py
  12. 50 0
      agent/core/prompts/compression.py
  13. 98 0
      agent/core/prompts/knowledge.py
  14. 39 0
      agent/core/prompts/runner.py
  15. 654 207
      agent/core/runner.py
  16. 1411 0
      agent/docs/architecture.md
  17. 169 0
      agent/docs/decisions.md
  18. 201 0
      agent/docs/knowledge.md
  19. 0 0
      agent/docs/multimodal.md
  20. 329 0
      agent/docs/scope-design.md
  21. 2 2
      agent/docs/skills.md
  22. 136 35
      agent/docs/tools.md
  23. 1 1
      agent/docs/trace-api.md
  24. 13 13
      agent/llm/openrouter.py
  25. 0 37
      agent/memory/__init__.py
  26. 0 106
      agent/memory/protocols.py
  27. 0 10
      agent/memory/skills/research.md
  28. 0 103
      agent/memory/stores.py
  29. 16 0
      agent/skill/__init__.py
  30. 4 82
      agent/skill/models.py
  31. 2 2
      agent/skill/skill_loader.py
  32. 7 2
      agent/skill/skills/browser.md
  33. 10 5
      agent/skill/skills/core.md
  34. 0 0
      agent/skill/skills/planning.md
  35. 419 0
      agent/skill/skills/research.md
  36. 8 4
      agent/tools/builtin/__init__.py
  37. 162 27
      agent/tools/builtin/bash.py
  38. 4 0
      agent/tools/builtin/browser/__init__.py
  39. 131 8
      agent/tools/builtin/browser/baseClass.py
  40. 0 487
      agent/tools/builtin/experience.py
  41. 4 0
      agent/tools/builtin/feishu/chat.py
  42. 5 0
      agent/tools/builtin/feishu/chat_history/chat_summary.json
  43. 60 0
      agent/tools/builtin/feishu/chat_history/chat_关涛.json
  44. 12 0
      agent/tools/builtin/feishu/chat_history/chat_孙若天.json
  45. 12 0
      agent/tools/builtin/feishu/chat_history/chat_谭景玉.json
  46. 1 1
      agent/tools/builtin/file/edit.py
  47. 1 1
      agent/tools/builtin/file/glob.py
  48. 1 1
      agent/tools/builtin/file/grep.py
  49. 1 1
      agent/tools/builtin/file/read.py
  50. 1 1
      agent/tools/builtin/file/write.py
  51. 1 1
      agent/tools/builtin/glob_tool.py
  52. 611 0
      agent/tools/builtin/knowledge.py
  53. 3 0
      agent/tools/builtin/sandbox.py
  54. 3 3
      agent/tools/builtin/skill.py
  55. 78 36
      agent/tools/builtin/subagent.py
  56. 94 11
      agent/tools/registry.py
  57. 15 5
      agent/tools/schema.py
  58. 12 55
      agent/trace/compaction.py
  59. 144 0
      agent/trace/examples_api.py
  60. 14 0
      agent/trace/goal_models.py
  61. 86 10
      agent/trace/goal_tool.py
  62. 114 0
      agent/trace/logs_websocket.py
  63. 44 4
      agent/trace/models.py
  64. 233 83
      agent/trace/run_api.py
  65. 44 18
      agent/trace/store.py
  66. 92 4
      agent/trace/websocket.py
  67. 7 0
      agent/utils/__init__.py
  68. 40 0
      agent/utils/logging.py
  69. 18 1
      api_server.py
  70. 28 26
      config/feishu_contacts.json
  71. 75 969
      docs/README.md
  72. 651 0
      docs/a2a-im.md
  73. 0 78
      docs/ref/Claude Code/agent-prompt-agent-creation-architect.md
  74. 0 12
      docs/ref/Claude Code/agent-prompt-bash-command-description-writer.md
  75. 0 9
      docs/ref/Claude Code/system-prompt-doing-tasks.md
  76. 0 6
      docs/ref/Claude Code/system-prompt-tool-usage-policy.md
  77. 0 16
      docs/ref/Claude Code/system-prompt-tool-use-summary-generation.md
  78. 0 0
      docs/ref/Claude Code/tool-description-bash.md
  79. 0 659
      docs/ref/context-comparison.md
  80. 0 98
      docs/ref/create.md
  81. 0 357
      docs/ref/deconstruct_old.md
  82. 0 31
      docs/ref/skills.md
  83. 71 0
      docs/research/README.md
  84. 733 0
      docs/research/a2a-continuous-dialogue.md
  85. 640 0
      docs/research/a2a-cross-device.md
  86. 504 0
      docs/research/a2a-mamp-protocol.md
  87. 114 0
      docs/research/a2a-protocols.md
  88. 484 0
      docs/research/a2a-trace-storage.md
  89. 0 0
      examples/archive/analyze_story/README.md
  90. 0 0
      examples/archive/analyze_story/analysis_results.json
  91. 0 0
      examples/archive/analyze_story/analyze_samples.py
  92. 0 0
      examples/archive/analyze_story/generate_report.py
  93. 0 0
      examples/archive/analyze_story/input/中国合伙人.pdf
  94. 0 0
      examples/archive/analyze_story/input/大奉打更人.txt
  95. 0 0
      examples/archive/analyze_story/input/搜神记.txt
  96. 0 0
      examples/archive/analyze_story/input/无双.docx
  97. 0 0
      examples/archive/analyze_story/input/雪中悍刀行.txt
  98. 0 0
      examples/archive/analyze_story/input/魔道祖师.txt
  99. 0 0
      examples/archive/analyze_story/knowledge/01_Scene_Sequel_Structure.md
  100. 0 0
      examples/archive/analyze_story/knowledge/01_save_the_cat_beat_sheet.md

+ 0 - 0
.browser_use_files/browseruse_agent_data/todo.md


+ 5 - 1
.claude/settings.local.json

@@ -11,7 +11,11 @@
       "Bash(timeout 60 python:*)",
       "Bash(timeout 60 python:*)",
       "Bash(timeout 240 python:*)",
       "Bash(timeout 240 python:*)",
       "Bash(curl:*)",
       "Bash(curl:*)",
-      "Bash(tree:*)"
+      "Bash(tree:*)",
+      "Bash(xargs grep:*)",
+      "Bash(npm run:*)",
+      "Bash(sed:*)",
+      "Bash(PYTHONIOENCODING=utf-8 python:*)"
     ],
     ],
     "deny": [],
     "deny": [],
     "ask": []
     "ask": []

+ 4 - 5
.env.template

@@ -1,8 +1,7 @@
-# 完成配置后,将 .env.template 重命名为 .env
-
+KNOWHUB_API=http://43.106.118.91:9999
+BROWSER_USE_API_KEY=
 
 
-# OpenRouter API Key,用于sonnet-4.6模型
 OPEN_ROUTER_API_KEY=
 OPEN_ROUTER_API_KEY=
+ALI_API_KEY=
+ALI_BASE_URL=
 
 
-# BrowserUse API Key
-BROWSER_USE_API_KEY=

+ 6 - 1
.gitignore

@@ -64,4 +64,9 @@ output
 examples/**/output*/
 examples/**/output*/
 
 
 frontend/htmlTemplate/mock_data
 frontend/htmlTemplate/mock_data
-frontend/react-template/yarn.lock
+frontend/react-template/yarn.lock
+
+# data
+knowhub/knowhub.db
+knowhub/knowhub.db-shm
+knowhub/knowhub.db-wal

+ 262 - 75
README.md

@@ -78,8 +78,12 @@ async def check_inventory(product_id: str, warehouse: str = "default") -> ToolRe
     "parameters": {
     "parameters": {
       "type": "object",
       "type": "object",
       "properties": {
       "properties": {
-        "product_id": {"type": "string", "description": "产品唯一标识符"},
-        "warehouse": {"type": "string", "description": "仓库编码,默认为主仓库", "default": "default"}
+        "product_id": { "type": "string", "description": "产品唯一标识符" },
+        "warehouse": {
+          "type": "string",
+          "description": "仓库编码,默认为主仓库",
+          "default": "default"
+        }
       },
       },
       "required": ["product_id"]
       "required": ["product_id"]
     }
     }
@@ -111,6 +115,7 @@ description: 领域专属知识
 ---
 ---
 
 
 ## Guidelines
 ## Guidelines
+
 - 规则 1
 - 规则 1
 - 规则 2
 - 规则 2
 ```
 ```
@@ -123,80 +128,95 @@ runner = AgentRunner(
 )
 )
 ```
 ```
 
 
-内置 skills(`agent/memory/skills/`)始终自动加载,`skills_dir` 的内容额外追加。
+内置 skills(`agent/skill/skills/`)始终自动加载,`skills_dir` 的内容额外追加。
 
 
-## 经验系统(Experience System
+## 知识管理系统(Knowledge Management
 
 
-经验系统通过**提取、注入、反馈、更新**四个环节,让 Agent 从历史执行中学习并持续改进
+知识管理系统通过**提取、存储、注入**三个环节,让 Agent 积累和复用结构化知识
 
 
 ### 核心流程
 ### 核心流程
 
 
 **1. 提取(Extract)**
 **1. 提取(Extract)**
-- **触发时机**:Level 2 压缩时自动触发
-- **提取方式**:在压缩历史消息前,先调用 LLM 对当前执行过程进行反思(reflect)
-- **输出格式**:ACE 规范经验条目
-  ```
-  当 [条件/Context] 时,应该 [动作/Action](原因:[逻辑/Reason])
-  ```
-- **存储位置**:追加到 `experiences.md` 文件(默认 `./.cache/experiences.md`)
-
-**2. 注入(Inject)**
-- **触发时机**:切换 Goal 时自动触发
-- **检索策略**:两阶段检索
-  - Stage 1: 语义路由(LLM 挑选 2*k 个相关经验)
-  - Stage 2: 质量精排(根据 metrics 筛选最终 k 个)
-- **注入方式**:将检索到的经验注入到主 Agent 的上下文中
-
-**3. 反馈(Feedback)**
-- **触发时机**:压缩时分析历史消息中经验的使用效果
-- **评价维度**:
-  - `helpful`: 经验有效,帮助完成任务
-  - `harmful`: 经验误导,导致错误
-  - `mixed`: 部分有效,需要改进
-- **反馈来源**:LLM 分析执行过程中经验的实际效果
-
-**4. 更新(Update)**
-- **Metrics 更新**:根据反馈调整 `helpful` 和 `harmful` 计数
-- **内容进化**:
-  - `helpful` + 有改进建议 → 触发经验重写(evolve)
-  - `harmful` 累积 → 降低检索权重或标记为有害
-- **质量过滤**:检索时自动过滤 `quality_score < -2` 的有害经验
-
-### 经验文件格式
 
 
-```markdown
----
-id: ex_02271430_a3f2
-trace_id: 6822d4e0-8aeb-449f-962e-c431c409a5a0
-tags: {intent: [解构, 图片分析], state: [多图]}
-metrics: {helpful: 3, harmful: 0}
-created_at: 2026-02-27 14:30:15
-updated_at: 2026-02-27 15:20:42
----
-当需要分析多张图片时,应该先并行读取所有图片再统一分析(原因:避免重复调用 LLM,节省 token 和时间)。
+- **触发时机**:
+  - 压缩时提取:消息量超阈值触发压缩时,在 Level 1 过滤前用完整 history 反思
+  - 完成时提取:Agent 运行完成后(不代表任务完成,可能中途退出等待人工评估)
+- **提取方式**:调用 LLM 对执行过程进行反思,提取可复用的知识
+- **自定义 Prompt**:可通过配置自定义反思 prompt,空则使用默认(见 `agent/core/prompts/knowledge.py`)
+
+**2. 存储(Store)**
+
+- **存储位置**:KnowHub 服务(默认 `http://localhost:8765`)
+- **知识结构**:
+  - `title`: 知识标题
+  - `content`: 知识内容
+  - `type`: 知识类型(strategy/tool/pattern/pitfall 等)
+  - `tags`: 标签(键值对,用于分类和检索)
+  - `scopes`: 作用域(如 `org:cybertogether`)
+  - `owner`: 所有者(默认从 git config user.email 获取)
+  - `resource_ids`: 关联资源 ID 列表(代码片段、凭证、cookies 等)
+- **资源管理**:
+  - 知识可关联多个资源(通过 `resource_ids` 字段)
+  - 资源包含 `body`(公开内容)和 `secure_body`(加密内容)
+  - 支持代码片段、API 凭证、cookies 等多种资源类型
+
+**3. 注入(Inject)**
+
+- **触发时机**:Agent 切换当前工作的 Goal 时自动触发
+- **检索策略**:基于 Goal 描述和上下文,从知识库检索相关知识
+- **注入方式**:将检索到的知识注入到 Agent 的上下文中
+
+### 配置
+
+知识管理配置通过 `RunConfig.knowledge` 传递:
+
+```python
+from agent.core.runner import KnowledgeConfig, RunConfig
+
+run_config = RunConfig(
+    model="claude-sonnet-4.5",
+    temperature=0.3,
+    max_iterations=1000,
+
+    knowledge=KnowledgeConfig(
+        # 压缩时提取(消息量超阈值触发压缩时,用完整 history 反思)
+        enable_extraction=True,
+        reflect_prompt="",  # 空则使用默认,见 agent/core/prompts/knowledge.py:REFLECT_PROMPT
+
+        # agent运行完成后提取
+        enable_completion_extraction=True,
+        completion_reflect_prompt="",  # 空则使用默认
+
+        # 知识注入(agent切换当前工作的goal时,自动注入相关知识)
+        enable_injection=True,
+
+        # 默认字段(保存/搜索时自动注入)
+        owner="",  # 空则从 git config user.email 获取(隐藏参数,LLM 不可见)
+        default_tags={"project": "my_project"},  # 与 LLM 传递的 tags 合并
+        default_scopes=["org:cybertogether"],  # 与 LLM 传递的 scopes 合并
+        default_search_types=["strategy", "tool"],
+        default_search_owner=""  # 空则不过滤
+    )
+)
 ```
 ```
 
 
-### 经验库瘦身
+**参数注入规则**(通过框架 `inject_params` 机制实现,详见 `agent/docs/tools.md`):
 
 
-`experience.py` 中提供 `slim_experiences()` 函数,可调用顶级 LLM 合并语义相似的经验,减少冗余。
+- `owner`:隐藏参数,LLM 不可见,框架自动注入(`mode: default`)
+- `tags`:LLM 可追加新 key,框架默认 key 不可被覆盖(`mode: merge`)
+- `scopes`:LLM 可追加,与框架默认值合并去重(`mode: merge`)
 
 
-**功能**:
-- 识别并合并语义高度相似的经验
-- 保留 helpful 最高的 ID
-- 合并 metrics(helpful/harmful 取各条之和)
-- 保持 ACE 规范格式
+### 知识工具
 
 
-**状态**:已实现但暂未自动调用,可在 `analyze_story/run.py` 的交互菜单中手动触发(选项 7)。
+框架提供以下内置工具用于知识管理:
 
 
-### 配置
+- `knowledge_save`: 保存知识到知识库
+- `knowledge_search`: 搜索知识库
+- `knowledge_get`: 获取指定知识详情
+- `resource_save`: 保存资源(代码、凭证等)
+- `resource_get`: 获取资源内容
 
 
-```python
-runner = AgentRunner(
-    llm_call=...,
-    trace_store=...,
-    experiences_path="./.cache/experiences.md",  # 自定义经验文件路径
-)
-```
+这些工具会自动注入配置的默认字段(owner, tags, scopes 等)。
 
 
 ## AgentRunner 参数
 ## AgentRunner 参数
 
 
@@ -206,9 +226,8 @@ AgentRunner(
     trace_store=None,        # Trace 持久化(推荐 FileSystemTraceStore)
     trace_store=None,        # Trace 持久化(推荐 FileSystemTraceStore)
     tool_registry=None,      # 工具注册表(默认:全局 registry)
     tool_registry=None,      # 工具注册表(默认:全局 registry)
     skills_dir=None,         # 自定义 skills 目录
     skills_dir=None,         # 自定义 skills 目录
-    experiences_path="./.cache/experiences.md",  # 经验文件路径
-    memory_store=None,       # 记忆存储
     utility_llm_call=None,   # 轻量 LLM(生成任务标题等)
     utility_llm_call=None,   # 轻量 LLM(生成任务标题等)
+    debug=False,             # 调试模式
 )
 )
 ```
 ```
 
 
@@ -224,9 +243,19 @@ RunConfig(
     agent_type="default",     # 预设类型:default / explore / analyst
     agent_type="default",     # 预设类型:default / explore / analyst
     trace_id=None,            # 续跑/回溯时传入已有 trace ID
     trace_id=None,            # 续跑/回溯时传入已有 trace ID
     after_sequence=None,      # 从哪条消息后续跑(message sequence)
     after_sequence=None,      # 从哪条消息后续跑(message sequence)
+    knowledge=KnowledgeConfig(),  # 知识管理配置
 )
 )
 ```
 ```
 
 
+    system_prompt=None,       # None=从 skills 自动构建
+    agent_type="default",     # 预设类型:default / explore / analyst
+    trace_id=None,            # 续跑/回溯时传入已有 trace ID
+    after_sequence=None,      # 从哪条消息后续跑(message sequence)
+
+)
+
+````
+
 ## LLM Providers
 ## LLM Providers
 
 
 框架内置两个 provider:
 框架内置两个 provider:
@@ -239,7 +268,7 @@ llm = create_openrouter_llm_call(model="anthropic/claude-sonnet-4.5")
 
 
 # Google Gemini
 # Google Gemini
 llm = create_gemini_llm_call(model="gemini-2.5-flash")
 llm = create_gemini_llm_call(model="gemini-2.5-flash")
-```
+````
 
 
 自定义 provider 只需实现签名:
 自定义 provider 只需实现签名:
 
 
@@ -262,15 +291,15 @@ async def my_llm_call(messages, model, tools, temperature, **kwargs) -> dict:
 python api_server.py
 python api_server.py
 ```
 ```
 
 
-| 方法 | 路径 | 说明 |
-|------|------|------|
-| GET | `/api/traces` | 列出 Traces |
-| GET | `/api/traces/{id}` | Trace 详情 |
-| GET | `/api/traces/{id}/messages` | 消息列表 |
-| POST | `/api/traces` | 新建并执行 |
-| POST | `/api/traces/{id}/run` | 续跑/回溯 |
-| POST | `/api/traces/{id}/stop` | 停止 |
-| WS | `/api/traces/{id}/watch` | 实时事件 |
+| 方法 | 路径                        | 说明        |
+| ---- | --------------------------- | ----------- |
+| GET  | `/api/traces`               | 列出 Traces |
+| GET  | `/api/traces/{id}`          | Trace 详情  |
+| GET  | `/api/traces/{id}/messages` | 消息列表    |
+| POST | `/api/traces`               | 新建并执行  |
+| POST | `/api/traces/{id}/run`      | 续跑/回溯   |
+| POST | `/api/traces/{id}/stop`     | 停止        |
+| WS   | `/api/traces/{id}/watch`    | 实时事件    |
 
 
 需在 `api_server.py` 中配置 Runner 才能启用 POST 端点。
 需在 `api_server.py` 中配置 Runner 才能启用 POST 端点。
 
 
@@ -281,8 +310,166 @@ agent/
 ├── core/           # AgentRunner + 预设
 ├── core/           # AgentRunner + 预设
 ├── tools/          # 工具系统(registry + 内置工具)
 ├── tools/          # 工具系统(registry + 内置工具)
 ├── trace/          # 执行追踪 + 计划(GoalTree)+ API
 ├── trace/          # 执行追踪 + 计划(GoalTree)+ API
-├── memory/         # Skills + Experiences
+├── skill/          # Skills(技能系统)
 └── llm/            # LLM Provider 适配
 └── llm/            # LLM Provider 适配
 ```
 ```
 
 
 详细架构文档:[docs/README.md](./docs/README.md)
 详细架构文档:[docs/README.md](./docs/README.md)
+
+## 交互式 CLI(Interactive CLI)
+
+框架提供交互式控制器,支持实时监控、手动干预和经验总结。
+
+### 使用方式
+
+```python
+from agent.cli import InteractiveController
+
+# 创建交互控制器
+interactive = InteractiveController(
+    runner=runner,
+    store=store,
+    enable_stdin_check=True  # 启用标准输入检查
+)
+
+# 在执行循环中检查用户输入
+async for item in runner.run(messages=messages, config=config):
+    cmd = interactive.check_stdin()
+    if cmd == 'pause':
+        await runner.stop(trace_id)
+        menu_result = await interactive.show_menu(trace_id, current_sequence)
+        # 处理菜单结果...
+    elif cmd == 'quit':
+        await runner.stop(trace_id)
+        break
+```
+
+### 交互控制
+
+在执行过程中,可以通过命令行实时控制:
+
+| 按键          | 动作         | 说明                              |
+| ------------- | ------------ | --------------------------------- |
+| `p` / `pause` | **暂停执行** | 立即挂起 Agent 循环,进入交互菜单 |
+| `q` / `quit`  | **停止执行** | 安全停止并保存当前的执行状态      |
+
+### 交互菜单功能
+
+进入暂停模式后,系统提供以下操作:
+
+1. **插入干预消息**:直接向 Agent 下达新指令
+2. **触发经验总结 (Reflect)**:强制 Agent 对当前过程进行反思
+3. **查看 GoalTree**:可视化当前任务的拆解结构和完成进度
+4. **上下文压缩 (Compact)**:手动精简对话历史
+
+### 项目配置示例
+
+完整的项目配置示例见 `examples/research/config.py`:
+
+```python
+from agent.core.runner import KnowledgeConfig, RunConfig
+from agent.utils import setup_logging
+
+# Agent 运行配置
+RUN_CONFIG = RunConfig(
+    model="claude-sonnet-4.5",
+    temperature=0.3,
+    max_iterations=1000,
+    name="Research Agent",
+
+    knowledge=KnowledgeConfig(
+        enable_extraction=True,
+        enable_completion_extraction=True,
+        enable_injection=True,
+        owner="",  # 空则从 git config 获取
+        default_tags={"project": "research"},
+        default_scopes=["org:cybertogether"],
+        default_search_types=["strategy", "tool"],
+    )
+)
+
+# 基础设施配置
+SKILLS_DIR = "./skills"
+TRACE_STORE_PATH = ".trace"
+DEBUG = True
+LOG_LEVEL = "INFO"
+LOG_FILE = None  # 可设置为文件路径
+
+# 在 run.py 中使用
+setup_logging(level=LOG_LEVEL, file=LOG_FILE)
+
+runner = AgentRunner(
+    trace_store=FileSystemTraceStore(base_path=TRACE_STORE_PATH),
+    llm_call=create_openrouter_llm_call(model=f"anthropic/{RUN_CONFIG.model}"),
+    skills_dir=SKILLS_DIR,
+    debug=DEBUG
+)
+
+async for item in runner.run(messages=messages, config=RUN_CONFIG):
+    # 处理执行结果
+    pass
+```
+
+**配置说明**:
+
+- 直接使用框架的 `RunConfig` 和 `KnowledgeConfig`,不需要自定义配置类
+- 基础设施配置(skills_dir, trace_store_path 等)用简单变量定义
+- 使用 `agent.utils.setup_logging()` 配置日志
+
+## 任务可视化与调试
+
+框架在运行期间会生成唯一的 `trace_id`。
+
+- **本地日志**:所有的执行细节、工具调用和 Goal 状态均持久化在 `.trace/` 目录下。
+- **Web 可视化**:
+
+1. 启动服务器:`python api_server.py`
+2. 启动前端:
+
+```
+  cd frontend/react-template
+  yarn
+  yarn dev
+```
+
+3. 访问控制台:`http://localhost:3000`
+4. 在前端界面中切换任务,即直观追踪 Agent 的思考链路。
+5. 因为该可视化读取的是根目录下的.trace文件,建议运行项目时,可以在根目录下用命令行运行`python examples/[project_name]/run.py`,使运行得到的trace存放在根目录
+
+### 提示:目前前端可视化只供观看本地运行过的trace结果,新任务运行等功能正在开发中,运行可在命令行中执行
+
+### 绿色节点为整体的goal(目标),蓝色节点为子goal(目标),灰色节点为基础信息节点。点击蓝色边/绿色边会折叠节点,点击节点会在右侧显示详情。
+
+---
+
+## 示例项目结构
+
+可以参考其他文件夹中的结构:
+
+```text
+examples/[your_example]/
+├── input/             # (可选)输入数据
+├── output_1/          # (可选)输出目录
+├── skills/            # (可选)领域专属 Skill (.md)
+├── tool/              # (可选)自定义工具
+├── presets.json       # (可选)预定义的子 Agent 配置
+├── config.py          # (推荐)项目配置
+├── [task].prompt      # (必须)任务 System Prompt 和 User Prompt
+└── run.py             # (必须)交互式运行入口
+```
+
+---
+
+## 环境兼容性
+
+针对 Clash Verge / TUN 模式等网络环境,本项目已内置代理自动避让逻辑:
+
+- **代理优化**:通过 `no_proxy` 配置防止 `httpx` 错误引导流量。
+- **Browser 模式**:支持 `cloud` (远程) 和 `local` (本地) 模式切换。
+
+## 运行结果存储
+
+运行过程中,会自动存储以下内容:
+
+- **运行轨迹**:根目录下 `.trace/` 文件夹下的实际运行路径结果
+- **知识库**:KnowHub 服务中保存的知识条目(通过 API 访问)

+ 167 - 0
agent/README.md

@@ -0,0 +1,167 @@
+# Agent Core
+
+**Agent 核心框架**:提供单个 Agent 的执行能力
+
+## 文档维护规范
+
+0. **先改文档,再动代码** - 新功能或重大修改需先完成文档更新、并完成审阅后,再进行代码实现;除非改动较小、不被文档涵盖
+1. **文档分层,链接代码** - 重要或复杂设计可以另有详细文档;关键实现需标注代码文件路径;格式:`module/file.py:function_name`
+2. **简洁快照,日志分离** - 只记录最重要的、与代码准确对应的或者明确的已完成的设计的信息,避免推测、建议,或大量代码;决策依据或修改日志若有必要,可在`docs/decisions.md`另行记录
+
+---
+
+## 概述
+
+Agent Core 是一个完整的 Agent 执行框架,提供:
+- Trace、Message、Goal 管理
+- 工具系统(文件、命令、网络、浏览器)
+- LLM 集成(Gemini、OpenRouter、Yescode)
+- Skills(领域知识注入)
+- 子 Agent 机制
+
+**独立性**:Agent Core 不依赖任何其他模块,可以独立运行。
+
+---
+
+## 模块结构
+
+```
+agent/
+├── core/                  # 核心引擎
+│   ├── runner.py          # AgentRunner + 运行时配置
+│   └── presets.py         # Agent 预设(explore、analyst 等)
+│
+├── trace/                 # 执行追踪(含计划管理)
+│   ├── models.py          # Trace, Message
+│   ├── goal_models.py     # Goal, GoalTree, GoalStats
+│   ├── protocols.py       # TraceStore 接口
+│   ├── store.py           # FileSystemTraceStore 实现
+│   ├── goal_tool.py       # goal 工具(计划管理)
+│   ├── compaction.py      # Context 压缩
+│   ├── api.py             # REST API
+│   └── websocket.py       # WebSocket API
+│
+├── tools/                 # 外部交互工具
+│   ├── registry.py        # 工具注册表
+│   ├── schema.py          # Schema 生成器
+│   ├── models.py          # ToolResult, ToolContext
+│   └── builtin/
+│       ├── file/          # 文件操作
+│       ├── browser/       # 浏览器自动化
+│       ├── bash.py        # 命令执行
+│       ├── subagent.py    # 子 Agent 创建
+│       └── a2a_im.py      # A2A IM 工具(桥接到 Gateway)
+│
+├── skill/                 # 技能系统
+│   ├── models.py          # Skill
+│   ├── skill_loader.py    # Skill 加载器
+│   └── skills/            # 内置 Skills
+│
+└── llm/                   # LLM 集成
+    ├── gemini.py          # Gemini Provider
+    ├── openrouter.py      # OpenRouter Provider
+    └── yescode.py         # Yescode Provider
+```
+
+---
+
+## 核心概念
+
+### Trace(任务执行)
+
+一次完整的 Agent 执行。所有 Agent(主、子、人类协助)都是 Trace。
+
+**实现位置**:`agent/trace/models.py:Trace`
+
+### Goal(目标节点)
+
+计划中的一个目标,支持层级结构。
+
+**实现位置**:`agent/trace/goal_models.py:Goal`
+
+### Message(执行消息)
+
+对应 LLM API 的消息,每条 Message 关联一个 Goal。
+
+**实现位置**:`agent/trace/models.py:Message`
+
+---
+
+## 快速开始
+
+### 基础使用
+
+```python
+from agent.core.runner import AgentRunner, RunConfig
+
+# 创建 Runner
+runner = AgentRunner(
+    llm_call=create_llm_call(),
+    trace_store=FileSystemTraceStore()
+)
+
+# 运行 Agent
+async for item in runner.run(
+    messages=[{"role": "user", "content": "分析项目架构"}],
+    config=RunConfig(model="gpt-4o")
+):
+    if isinstance(item, Trace):
+        print(f"Trace: {item.trace_id}")
+    elif isinstance(item, Message):
+        print(f"Message: {item.content}")
+```
+
+### 使用工具
+
+```python
+from agent.tools import tool, ToolContext, ToolResult
+
+@tool(description="自定义工具")
+async def my_tool(arg: str, ctx: ToolContext) -> ToolResult:
+    return ToolResult(
+        title="成功",
+        output=f"处理结果: {arg}"
+    )
+```
+
+---
+
+## 文档
+
+### 模块文档(agent/docs/)
+
+- [架构设计](./docs/architecture.md):Agent Core 完整架构设计
+- [工具系统](./docs/tools.md):工具定义、注册、双层记忆
+- [Skills 指南](./docs/skills.md):Skill 分类、编写、加载
+- [Trace API](./docs/trace-api.md):Trace 模块 REST API 和 WebSocket 接口
+- [多模态支持](./docs/multimodal.md):图片、PDF 处理
+- [设计决策](./docs/decisions.md):架构决策记录
+
+### 项目级文档(../docs/)
+
+- [项目总览](../docs/README.md):完整的架构设计文档
+- [A2A IM 系统](../docs/a2a-im.md):Agent 间通讯
+- [Enterprise 层](../gateway/docs/enterprise/overview.md):组织级功能
+
+---
+
+## API
+
+### REST API
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| GET  | `/api/traces` | 列出 Traces |
+| GET  | `/api/traces/{id}` | 获取 Trace 详情 |
+| GET  | `/api/traces/{id}/messages` | 获取 Messages |
+| POST | `/api/traces` | 新建 Trace 并执行 |
+| POST | `/api/traces/{id}/run` | 续跑或回溯 |
+| POST | `/api/traces/{id}/stop` | 停止运行 |
+
+**实现位置**:`agent/trace/api.py`, `agent/trace/run_api.py`
+
+---
+
+## 相关项目
+
+- [Gateway](../gateway/README.md):A2A IM Gateway,提供 Agent 间通讯能力

+ 7 - 15
agent/__init__.py

@@ -3,15 +3,15 @@ Reson Agent - 模块化、可扩展的 Agent 框架
 
 
 核心导出:
 核心导出:
 - AgentRunner: Agent 执行引擎
 - AgentRunner: Agent 执行引擎
-- AgentConfig: Agent 配置
+- RunConfig: 运行配置
 - Trace, Message, Goal: 执行追踪
 - Trace, Message, Goal: 执行追踪
-- Experience, Skill: 记忆模型
+- Skill: 技能模型
 - tool: 工具装饰器
 - tool: 工具装饰器
-- TraceStore, MemoryStore: 存储接口
+- TraceStore: 存储接口
 """
 """
 
 
 # 核心引擎
 # 核心引擎
-from agent.core.runner import AgentRunner, AgentConfig, CallResult, RunConfig
+from agent.core.runner import AgentRunner, CallResult, RunConfig
 from agent.core.presets import AgentPreset, AGENT_PRESETS, get_preset
 from agent.core.presets import AgentPreset, AGENT_PRESETS, get_preset
 
 
 # 执行追踪
 # 执行追踪
@@ -20,10 +20,8 @@ from agent.trace.goal_models import Goal, GoalTree, GoalStatus
 from agent.trace.protocols import TraceStore
 from agent.trace.protocols import TraceStore
 from agent.trace.store import FileSystemTraceStore
 from agent.trace.store import FileSystemTraceStore
 
 
-# 记忆系统
-from agent.memory.models import Experience, Skill
-from agent.memory.protocols import MemoryStore, StateStore
-from agent.memory.stores import MemoryMemoryStore, MemoryStateStore
+# 技能系统
+from agent.skill.models import Skill
 
 
 # 工具系统
 # 工具系统
 from agent.tools import tool, ToolRegistry, get_tool_registry
 from agent.tools import tool, ToolRegistry, get_tool_registry
@@ -34,7 +32,6 @@ __version__ = "0.3.0"
 __all__ = [
 __all__ = [
     # Core
     # Core
     "AgentRunner",
     "AgentRunner",
-    "AgentConfig",
     "CallResult",
     "CallResult",
     "RunConfig",
     "RunConfig",
     "AgentPreset",
     "AgentPreset",
@@ -54,13 +51,8 @@ __all__ = [
     "GoalStatus",
     "GoalStatus",
     "TraceStore",
     "TraceStore",
     "FileSystemTraceStore",
     "FileSystemTraceStore",
-    # Memory
-    "Experience",
+    # Skill
     "Skill",
     "Skill",
-    "MemoryStore",
-    "StateStore",
-    "MemoryMemoryStore",
-    "MemoryStateStore",
     # Tools
     # Tools
     "tool",
     "tool",
     "ToolRegistry",
     "ToolRegistry",

+ 11 - 0
agent/cli/__init__.py

@@ -0,0 +1,11 @@
+"""
+CLI 工具模块
+
+提供交互式控制等 CLI 相关功能。
+"""
+
+from .interactive import InteractiveController
+
+__all__ = [
+    "InteractiveController",
+]

+ 274 - 0
agent/cli/interactive.py

@@ -0,0 +1,274 @@
+"""
+交互式控制器
+
+提供暂停/继续、交互式菜单、经验总结等功能。
+"""
+
+import sys
+import asyncio
+from typing import Optional, Dict, Any
+from pathlib import Path
+
+from agent.core.runner import AgentRunner
+from agent.trace import TraceStore
+
+
+# ===== 非阻塞 stdin 检测 =====
+
+if sys.platform == 'win32':
+    import msvcrt
+
+
+def check_stdin() -> Optional[str]:
+    """
+    跨平台非阻塞检查 stdin 输入。
+
+    Windows: 使用 msvcrt.kbhit()
+    macOS/Linux: 使用 select.select()
+
+    Returns:
+        'pause' | 'quit' | None
+    """
+    if sys.platform == 'win32':
+        # Windows: 检查是否有按键按下
+        if msvcrt.kbhit():
+            ch = msvcrt.getwch().lower()
+            if ch == 'p':
+                return 'pause'
+            if ch == 'q':
+                return 'quit'
+        return None
+    else:
+        # Unix/Mac: 使用 select
+        import select
+        ready, _, _ = select.select([sys.stdin], [], [], 0)
+        if ready:
+            line = sys.stdin.readline().strip().lower()
+            if line in ('p', 'pause'):
+                return 'pause'
+            if line in ('q', 'quit'):
+                return 'quit'
+        return None
+
+
+def read_multiline() -> str:
+    """
+    读取多行输入,以连续两次回车(空行)结束。
+
+    Returns:
+        用户输入的多行文本
+    """
+    print("\n请输入干预消息(连续输入两次回车结束):")
+    lines = []
+    blank_count = 0
+
+    while True:
+        line = input()
+        if line == "":
+            blank_count += 1
+            if blank_count >= 2:
+                break
+            lines.append("")  # 保留单个空行
+        else:
+            blank_count = 0
+            lines.append(line)
+
+    # 去掉尾部多余空行
+    while lines and lines[-1] == "":
+        lines.pop()
+
+    return "\n".join(lines)
+
+
+# ===== 交互式控制器 =====
+
+class InteractiveController:
+    """
+    交互式控制器
+
+    管理暂停/继续、交互式菜单、经验总结等交互功能。
+    """
+
+    def __init__(
+        self,
+        runner: AgentRunner,
+        store: TraceStore,
+        enable_stdin_check: bool = True
+    ):
+        """
+        初始化交互式控制器
+
+        Args:
+            runner: Agent Runner 实例
+            store: Trace Store 实例
+            enable_stdin_check: 是否启用 stdin 检查
+        """
+        self.runner = runner
+        self.store = store
+        self.enable_stdin_check = enable_stdin_check
+
+    def check_stdin(self) -> Optional[str]:
+        """
+        检查 stdin 输入
+
+        Returns:
+            'pause' | 'quit' | None
+        """
+        if not self.enable_stdin_check:
+            return None
+        return check_stdin()
+
+    async def show_menu(
+        self,
+        trace_id: str,
+        current_sequence: int
+    ) -> Dict[str, Any]:
+        """
+        显示交互式菜单
+
+        Args:
+            trace_id: Trace ID
+            current_sequence: 当前消息序号
+
+        Returns:
+            用户选择的操作
+        """
+        print("\n" + "=" * 60)
+        print("  执行已暂停")
+        print("=" * 60)
+        print("请选择操作:")
+        print("  1. 插入干预消息并继续")
+        print("  2. 触发经验总结(reflect)")
+        print("  3. 查看当前 GoalTree")
+        print("  4. 手动压缩上下文(compact)")
+        print("  5. 继续执行")
+        print("  6. 停止执行")
+        print("=" * 60)
+
+        while True:
+            choice = input("请输入选项 (1-6): ").strip()
+
+            if choice == "1":
+                # 插入干预消息
+                text = read_multiline()
+                if not text:
+                    print("未输入任何内容,取消操作")
+                    continue
+
+                print(f"\n将插入干预消息并继续执行...")
+                # 从 store 读取实际的 last_sequence
+                live_trace = await self.store.get_trace(trace_id)
+                actual_sequence = live_trace.last_sequence if live_trace and live_trace.last_sequence else current_sequence
+
+                return {
+                    "action": "continue",
+                    "messages": [{"role": "user", "content": text}],
+                    "after_sequence": actual_sequence,
+                }
+
+            elif choice == "2":
+                # 触发经验总结
+                print("\n触发经验总结...")
+                focus = input("请输入反思重点(可选,直接回车跳过): ").strip()
+                await self.perform_reflection(trace_id, focus=focus)
+                continue
+
+            elif choice == "3":
+                # 查看 GoalTree
+                goal_tree = await self.store.get_goal_tree(trace_id)
+                if goal_tree and goal_tree.goals:
+                    print("\n当前 GoalTree:")
+                    print(goal_tree.to_prompt())
+                else:
+                    print("\n当前没有 Goal")
+                continue
+
+            elif choice == "4":
+                # 手动压缩上下文
+                await self.manual_compact(trace_id)
+                continue
+
+            elif choice == "5":
+                # 继续执行
+                print("\n继续执行...")
+                return {"action": "continue"}
+
+            elif choice == "6":
+                # 停止执行
+                print("\n停止执行...")
+                return {"action": "stop"}
+
+            else:
+                print("无效选项,请重新输入")
+
+    async def perform_reflection(
+        self,
+        trace_id: str,
+        focus: str = ""
+    ):
+        """
+        执行经验总结
+
+        通过调用 API 端点触发反思侧分支。
+
+        Args:
+            trace_id: Trace ID
+            focus: 反思重点(可选)
+        """
+        import httpx
+
+        print("正在启动反思任务...")
+
+        try:
+            # 调用 reflect API 端点
+            async with httpx.AsyncClient() as client:
+                payload = {}
+                if focus:
+                    payload["focus"] = focus
+
+                response = await client.post(
+                    f"http://localhost:8000/api/traces/{trace_id}/reflect",
+                    json=payload,
+                    timeout=10.0
+                )
+                response.raise_for_status()
+                result = response.json()
+
+            print(f"✅ 反思任务已启动: {result.get('message', '')}")
+            print("提示:可通过 WebSocket 监听实时进度")
+
+        except httpx.HTTPError as e:
+            print(f"❌ 反思任务启动失败: {e}")
+        except Exception as e:
+            print(f"❌ 发生错误: {e}")
+
+    async def manual_compact(self, trace_id: str):
+        """
+        手动压缩上下文
+
+        通过调用 API 端点触发压缩侧分支。
+
+        Args:
+            trace_id: Trace ID
+        """
+        import httpx
+
+        print("\n正在启动上下文压缩任务...")
+
+        try:
+            # 调用 compact API 端点
+            async with httpx.AsyncClient() as client:
+                response = await client.post(
+                    f"http://localhost:8000/api/traces/{trace_id}/compact",
+                    timeout=10.0
+                )
+                response.raise_for_status()
+                result = response.json()
+
+            print(f"✅ 压缩任务已启动: {result.get('message', '')}")
+            print("提示:可通过 WebSocket 监听实时进度")
+
+        except httpx.HTTPError as e:
+            print(f"❌ 压缩任务启动失败: {e}")
+        except Exception as e:
+            print(f"❌ 发生错误: {e}")

+ 2 - 3
agent/core/__init__.py

@@ -3,17 +3,16 @@ Agent Core - 核心引擎模块
 
 
 职责:
 职责:
 1. Agent 主循环逻辑(call() 和 run())
 1. Agent 主循环逻辑(call() 和 run())
-2. 配置数据类(AgentConfig, CallResult)
+2. 配置数据类(CallResult, RunConfig
 3. Agent 预设(AgentPreset)
 3. Agent 预设(AgentPreset)
 """
 """
 
 
-from agent.core.runner import AgentRunner, BUILTIN_TOOLS, AgentConfig, CallResult, RunConfig
+from agent.core.runner import AgentRunner, BUILTIN_TOOLS, CallResult, RunConfig
 from agent.core.presets import AgentPreset, AGENT_PRESETS, get_preset, register_preset
 from agent.core.presets import AgentPreset, AGENT_PRESETS, get_preset, register_preset
 
 
 __all__ = [
 __all__ = [
     "AgentRunner",
     "AgentRunner",
     "BUILTIN_TOOLS",
     "BUILTIN_TOOLS",
-    "AgentConfig",
     "CallResult",
     "CallResult",
     "RunConfig",
     "RunConfig",
     "AgentPreset",
     "AgentPreset",

+ 58 - 0
agent/core/prompts/__init__.py

@@ -0,0 +1,58 @@
+"""
+agent.core.prompts - Agent 系统 Prompt 集中管理
+
+子模块:
+- runner.py     系统提示、工具中断、任务命名、经验格式
+- knowledge.py  知识反思提取(压缩时 + 任务完成后)
+- compression.py  消息压缩总结
+- subagent.py   子 Agent 评估、结果格式化、知识管理
+"""
+
+from agent.core.prompts.runner import (
+    DEFAULT_SYSTEM_PREFIX,
+    TRUNCATION_HINT,
+    TOOL_INTERRUPTED_MESSAGE,
+    AGENT_INTERRUPTED_SUMMARY,
+    AGENT_CONTINUE_HINT_TEMPLATE,
+    TASK_NAME_GENERATION_SYSTEM_PROMPT,
+    TASK_NAME_FALLBACK,
+    build_tool_interrupted_message,
+    build_agent_continue_hint,
+)
+
+from agent.core.prompts.knowledge import (
+    REFLECT_PROMPT,
+    COMPLETION_REFLECT_PROMPT,
+    build_reflect_prompt,
+)
+
+from agent.core.prompts.compression import (
+    COMPRESSION_PROMPT_TEMPLATE,
+    COMPRESSION_EVAL_PROMPT_TEMPLATE,
+    SUMMARY_HEADER_TEMPLATE,
+    build_compression_eval_prompt,
+    build_summary_header,
+)
+
+__all__ = [
+    # runner
+    "DEFAULT_SYSTEM_PREFIX",
+    "TRUNCATION_HINT",
+    "TOOL_INTERRUPTED_MESSAGE",
+    "AGENT_INTERRUPTED_SUMMARY",
+    "AGENT_CONTINUE_HINT_TEMPLATE",
+    "TASK_NAME_GENERATION_SYSTEM_PROMPT",
+    "TASK_NAME_FALLBACK",
+    "build_tool_interrupted_message",
+    "build_agent_continue_hint",
+    # knowledge
+    "REFLECT_PROMPT",
+    "COMPLETION_REFLECT_PROMPT",
+    "build_reflect_prompt",
+    # compression
+    "COMPRESSION_PROMPT_TEMPLATE",
+    "COMPRESSION_EVAL_PROMPT_TEMPLATE",
+    "SUMMARY_HEADER_TEMPLATE",
+    "build_compression_eval_prompt",
+    "build_summary_header",
+]

+ 50 - 0
agent/core/prompts/compression.py

@@ -0,0 +1,50 @@
+"""
+压缩相关 Prompt
+
+包含 Level 2 消息压缩(LLM 总结)使用的 prompt。
+"""
+
+# ===== 压缩总结 =====
+
+COMPRESSION_PROMPT_TEMPLATE = """请对以上对话历史进行压缩总结。
+
+### 摘要要求
+1. 保留关键决策、结论和产出(如创建的文件、修改的代码、得出的分析结论)
+2. 保留重要的上下文(如用户的要求、约束条件、之前的讨论结果)
+3. 省略中间探索过程、重复的工具调用细节
+4. 使用结构化格式(标题 + 要点 + 相关资源引用,若有)
+5. 控制在 2000 字以内
+
+当前 GoalTree 状态:
+{goal_tree_prompt}
+
+格式要求:
+[[SUMMARY]]
+(此处填写结构化的摘要内容)
+"""
+
+# 保留旧名以兼容 compaction.py 的调用
+COMPRESSION_EVAL_PROMPT_TEMPLATE = COMPRESSION_PROMPT_TEMPLATE
+
+SUMMARY_HEADER_TEMPLATE = """## 对话历史摘要(自动压缩)
+
+{summary_text}
+
+---
+*以上为压缩摘要,原始对话历史已归档。*
+"""
+
+# ===== 辅助函数 =====
+
+def build_compression_eval_prompt(
+    goal_tree_prompt: str,
+    ex_reference_list: str = "",
+) -> str:
+    return COMPRESSION_EVAL_PROMPT_TEMPLATE.format(
+        goal_tree_prompt=goal_tree_prompt,
+        ex_reference_list=ex_reference_list,
+    )
+
+
+def build_summary_header(summary_text: str) -> str:
+    return SUMMARY_HEADER_TEMPLATE.format(summary_text=summary_text)

+ 98 - 0
agent/core/prompts/knowledge.py

@@ -0,0 +1,98 @@
+"""
+知识提取相关 Prompt
+
+两个场景,各自独立配置:
+- REFLECT_PROMPT:            压缩时阶段性反思(消息量超阈值,对当前批历史提炼)
+- COMPLETION_REFLECT_PROMPT: 任务完成后全局复盘(对整个任务的全局视角)
+
+两个 prompt 都要求 LLM 直接调用 `knowledge_save` 工具保存经验,
+而不是输出结构化文本再由 runner 解析。
+"""
+
+# ===== 压缩时阶段性反思 =====
+
+REFLECT_PROMPT = """请回顾以上执行过程,将值得沉淀的经验直接用 `knowledge_save` 工具保存到知识库。
+
+**关注以下方面**:
+1. 人工干预:用户中途的指令说明了哪里出了问题
+2. 弯路:哪些尝试是不必要的,有没有更直接的方法
+3. 好的决策:哪些判断和选择是正确的,值得记住
+4. 工具使用:哪些工具用法是高效的,哪些可以改进
+5. **资源发现**:是否发现了有价值的资源需要保存(见下方说明)
+
+**每条经验调用一次 `knowledge_save`,参数说明**:
+- `task`: 这条经验适用的场景,格式:「在[什么情境]下,[要完成什么]」
+- `content`: 具体经验内容,格式:「当[条件]时,应该[动作](原因:[一句话])。案例:[具体案例]」
+- `types`: 选 `["strategy"]`;如果涉及工具用法也可加 `"tool"`
+- `tags`: 用 `intent`(任务意图)和 `state`(环境状态/相关工具名)标注,便于检索
+- `score`: 1-5,根据这条经验的价值评估
+- `resource_ids`: 如果关联了资源,填写资源 ID 列表(可选)
+
+**资源提取指南**:
+如果执行过程中涉及以下内容,应先用 `resource_save` 保存资源,再用 `knowledge_save` 提交相关的经验/知识:
+
+1. **复杂代码工具**(逻辑复杂、超过 100 行):
+   - 调用 `resource_save(resource_id="code/{category}/{name}", title="...", body="代码内容", content_type="code", metadata={"language": "python"})`
+   - 然后在 `knowledge_save` 中通过 `resource_ids=["code/{category}/{name}"]` 关联
+
+2. **账号密码凭证**:
+   - 调用 `resource_save(resource_id="credentials/{website}", title="...", body="使用说明", secure_body="账号:xxx\\n密码:xxx", content_type="credential", metadata={"acquired_at": "2026-03-06T10:00:00Z"})`
+   - 然后在 `knowledge_save` 中通过 `resource_ids=["credentials/{website}"]` 关联
+
+3. **Cookie 和登录态**:
+   - 调用 `resource_save(resource_id="cookies/{website}", title="...", body="获取方法", secure_body="cookie内容", content_type="cookie", metadata={"acquired_at": "...", "expires_at": "..."})`
+   - 然后在 `knowledge_save` 中通过 `resource_ids=["cookies/{website}"]` 关联
+
+4. **多资源引用**:
+   - 一个知识可以关联多个资源,如:`resource_ids=["code/selenium/login", "credentials/website_a"]`
+
+**注意**:
+- 只保存最有价值的经验,宁少勿滥;一次就成功或比较简单的经验就不要记录了,记录反复尝试或被用户指导后才成功的经验、或者是调研之后的收获。
+- 不需要输出任何文字,直接调用工具即可
+- 如果没有值得保存的经验,不调用任何工具
+"""
+
+
+# ===== 任务完成后全局复盘 =====
+
+COMPLETION_REFLECT_PROMPT = """请对整个任务进行复盘,将值得沉淀的经验直接用 `knowledge_save` 工具保存到知识库。
+
+与压缩时的阶段性反思不同,这是任务结束后的全局视角,关注:
+1. 任务整体路径:实际走的路径与最初计划的偏差
+2. 关键决策点:哪些决策显著影响了最终结果
+3. 可复用的模式:哪些做法在类似任务中可以直接复用
+4. 踩过的坑:哪些问题本可提前规避
+5. **资源沉淀**:任务中产生或发现的有价值资源(见下方说明)
+
+**每条经验调用一次 `knowledge_save`,参数说明**:
+- `task`: 这条经验适用的场景,格式:「在[什么情境]下,[要完成什么]」
+- `content`: 具体经验内容,格式:「当[条件]时,应该[动作](原因:[一句话])。案例:[具体案例]」
+- `types`: 选 `["strategy"]`;如果涉及工具用法也可加 `"tool"`
+- `tags`: 用 `intent`(任务意图)和 `state`(环境状态/相关工具名)标注,便于检索
+- `score`: 1-5,根据这条经验的价值评估
+- `resource_ids`: 如果关联了资源,填写资源 ID 列表(可选)
+
+**资源提取指南**:
+如果任务中涉及以下内容,应先用 `resource_save` 保存资源,再用 `knowledge_save` 关联:
+
+1. **复杂代码工具**(逻辑复杂、超过 20 行、可复用):
+   - 调用 `resource_save(resource_id="code/{category}/{name}", title="...", body="代码内容", content_type="code", metadata={"language": "python"})`
+   - 然后在 `knowledge_save` 中通过 `resource_id` 关联
+
+2. **账号密码凭证**:
+   - 调用 `resource_save(resource_id="credentials/{website}", title="...", body="使用说明", secure_body="账号:xxx\\n密码:xxx", content_type="credential", metadata={"acquired_at": "2026-03-06T10:00:00Z"})`
+   - 然后在 `knowledge_save` 中通过 `secure_resource_id` 关联
+
+3. **Cookie 和登录态**:
+   - 调用 `resource_save(resource_id="cookies/{website}", title="...", body="获取方法", secure_body="cookie内容", content_type="cookie", metadata={"acquired_at": "...", "expires_at": "..."})`
+   - 然后在 `knowledge_save` 中通过 `secure_resource_id` 关联
+
+**注意**:
+- 只保存最有价值的经验,宁少勿滥;一次就成功或比较简单的经验就不要记录了,记录反复尝试或被用户指导后才成功的经验、或者是调研之后的收获。
+- 不需要输出任何文字,直接调用工具即可
+- 如果没有值得保存的经验,不调用任何工具
+"""
+
+
+def build_reflect_prompt() -> str:
+    return REFLECT_PROMPT

+ 39 - 0
agent/core/prompts/runner.py

@@ -0,0 +1,39 @@
+"""
+Runner 相关 Prompt
+
+包含 AgentRunner 主循环使用的 prompt:
+- 系统提示前缀
+- 工具执行中断提示
+- 任务名称生成
+- 经验条目格式
+"""
+
+# ===== 系统提示 =====
+
+DEFAULT_SYSTEM_PREFIX = "你是最顶尖的AI助手,可以拆分并调用工具逐步解决复杂问题。"
+
+# ===== 工具执行 =====
+
+TRUNCATION_HINT = """你的响应因为 max_tokens 限制被截断,tool call 参数不完整,未执行。请将大内容拆分为多次小的工具调用(例如用 write_file 的 append 模式分批写入)。"""
+
+TOOL_INTERRUPTED_MESSAGE = """⚠️ 工具 {tool_name} 执行被中断(进程异常退出),未获得执行结果。请根据需要重新调用。"""
+
+AGENT_INTERRUPTED_SUMMARY = "⚠️ 子Agent执行被中断(进程异常退出)"
+
+AGENT_CONTINUE_HINT_TEMPLATE = '使用 continue_from="{sub_trace_id}" 可继续执行,保留已有进度'
+
+# ===== 任务命名 =====
+
+TASK_NAME_GENERATION_SYSTEM_PROMPT = "用中文为以下任务生成一个简短标题(10-30字),只输出标题本身:"
+
+TASK_NAME_FALLBACK = "未命名任务"
+
+# ===== 辅助函数 =====
+
+def build_tool_interrupted_message(tool_name: str) -> str:
+    return TOOL_INTERRUPTED_MESSAGE.format(tool_name=tool_name)
+
+
+def build_agent_continue_hint(sub_trace_id: str) -> str:
+    return AGENT_CONTINUE_HINT_TEMPLATE.format(sub_trace_id=sub_trace_id)
+

Разлика између датотеке није приказан због своје велике величине
+ 654 - 207
agent/core/runner.py


+ 1411 - 0
agent/docs/architecture.md

@@ -0,0 +1,1411 @@
+# Agent Core 架构设计
+
+本文档描述 Agent Core 模块的完整架构设计。
+
+## 文档维护规范
+
+0. **先改文档,再动代码** - 新功能或重大修改需先完成文档更新、并完成审阅后,再进行代码实现;除非改动较小、不被文档涵盖
+1. **文档分层,链接代码** - 重要或复杂设计可以另有详细文档;关键实现需标注代码文件路径;格式:`module/file.py:function_name`
+2. **简洁快照,日志分离** - 只记录最重要的、与代码准确对应的或者明确的已完成的设计的信息,避免推测、建议,或大量代码;决策依据或修改日志若有必要,可在 `decisions.md` 另行记录
+
+---
+
+## 系统概览
+
+**核心理念:所有 Agent 都是 Trace**
+
+| 类型 | 创建方式 | 父子关系 | 状态 |
+|------|---------|---------|------|
+| 主 Agent | 直接调用 `runner.run()` | 无 parent | 正常执行 |
+| 子 Agent | 通过 `agent` 工具 | `parent_trace_id` / `parent_goal_id` 指向父 | 正常执行 |
+| 人类协助 | 通过 `ask_human` 工具 | `parent_trace_id` 指向父 | 阻塞等待 |
+
+---
+
+## 核心架构
+
+### 模块结构
+
+```
+agent/
+├── core/                  # 核心引擎
+│   ├── runner.py          # AgentRunner + 运行时配置
+│   └── presets.py         # Agent 预设(explore、analyst 等)
+│
+├── trace/                 # 执行追踪(含计划管理)
+│   ├── models.py          # Trace, Message
+│   ├── goal_models.py     # Goal, GoalTree, GoalStats
+│   ├── protocols.py       # TraceStore 接口
+│   ├── store.py           # FileSystemTraceStore 实现
+│   ├── goal_tool.py       # goal 工具(计划管理)
+│   ├── compaction.py      # Context 压缩
+│   ├── api.py             # REST API
+│   ├── websocket.py       # WebSocket API
+│   └── trace_id.py        # Trace ID 生成工具
+│
+├── tools/                 # 外部交互工具
+│   ├── registry.py        # 工具注册表
+│   ├── schema.py          # Schema 生成器
+│   ├── models.py          # ToolResult, ToolContext
+│   └── builtin/
+│       ├── file/          # 文件操作(read, write, edit, glob, grep)
+│       ├── browser/       # 浏览器自动化
+│       ├── bash.py        # 命令执行
+│       ├── sandbox.py     # 沙箱环境
+│       ├── search.py      # 网络搜索
+│       ├── webfetch.py    # 网页抓取
+│       ├── skill.py       # 技能加载
+│       └── subagent.py    # agent / evaluate 工具(子 Agent 创建与评估)
+│
+├── skill/                 # 技能系统
+│   ├── models.py          # Skill
+│   ├── skill_loader.py    # Skill 加载器
+│   └── skills/            # 内置 Skills(自动注入 system prompt)
+│       ├── planning.md    # 计划与 Goal 工具使用
+│       ├── research.md    # 搜索与内容研究
+│       └── browser.md     # 浏览器自动化
+│
+├── llm/                   # LLM 集成
+│   ├── gemini.py          # Gemini Provider
+│   ├── openrouter.py      # OpenRouter Provider(OpenAI 兼容格式)
+│   ├── yescode.py         # Yescode Provider(Anthropic 原生 Messages API)
+│   └── prompts/           # Prompt 工具
+```
+
+### 职责划分
+
+| 模块 | 职责 |
+|-----|------|
+| **core/** | Agent 执行引擎 + 预设配置 |
+| **trace/** | 执行追踪 + 计划管理 |
+| **tools/** | 与外部世界交互(文件、命令、网络、浏览器) |
+| **skill/** | 技能系统(Skills) |
+| **llm/** | LLM Provider 适配 |
+
+### 三层记忆模型
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ Layer 3: Skills(技能库)                                     │
+│ - Markdown 文件,存储领域知识和能力描述                        │
+│ - 通过 skill 工具按需加载到对话历史                            │
+└─────────────────────────────────────────────────────────────┘
+                              ▲
+                              │ 归纳
+┌─────────────────────────────────────────────────────────────┐
+│ Layer 2: Experience(经验库)                                 │
+│ - 数据库存储,条件 + 规则 + 证据                              │
+│ - 向量检索,注入到 system prompt                              │
+└─────────────────────────────────────────────────────────────┘
+                              ▲
+                              │ 提取
+┌─────────────────────────────────────────────────────────────┐
+│ Layer 1: Trace(任务状态)                                    │
+│ - 当前任务的工作记忆                                          │
+│ - Trace + Messages 记录执行过程                               │
+│ - Goals 管理执行计划                                          │
+└─────────────────────────────────────────────────────────────┘
+```
+
+### LLM Provider 适配
+
+#### 内部格式
+
+框架内部统一使用 OpenAI 兼容格式(`List[Dict]`)存储和传递消息。各 Provider 负责双向转换:
+
+| 方向 | 说明 |
+|------|------|
+| 入(LLM 响应 → 框架) | 提取 content、tool_calls、usage,转换为统一 Dict |
+| 出(框架 → LLM 请求) | OpenAI 格式消息列表 → 各 API 原生格式 |
+
+#### 工具消息分组
+
+存储层每个 tool result 独立一条 Message(OpenAI 格式最大公约数)。各 Provider 在出方向按 API 要求自行分组:
+
+| Provider | 分组方式 |
+|----------|---------|
+| OpenRouter | 无需分组(OpenAI 原生支持独立 tool 消息) |
+| Yescode | `_convert_messages_to_anthropic` 合并连续 tool 消息为单个 user message |
+| Gemini | `_convert_messages_to_gemini` 通过 buffer 合并连续 tool 消息 |
+
+#### 跨 Provider 续跑:tool_call_id 规范化
+
+不同 Provider 生成的 tool_call_id 格式不同(OpenAI: `call_xxx`,Anthropic: `toolu_xxx`,Gemini: 合成 `call_0`)。存储层按原样保存,不做规范化。
+
+跨 Provider 续跑时,出方向转换前检测历史中的 tool_call_id 格式,不兼容时统一重写为目标格式(保持 tool_use / tool_result 配对一致)。同格式跳过,零开销。Gemini 按 function name 匹配,无需重写。
+
+**实现**:`agent/llm/openrouter.py:_normalize_tool_call_ids`, `agent/llm/yescode.py:_normalize_tool_call_ids`
+
+---
+
+## 核心流程:Agent Loop
+
+### 参数分层
+
+```
+Layer 1: Infrastructure(基础设施,AgentRunner 构造时设置)
+  trace_store, memory_store, tool_registry, llm_call, skills_dir, utility_llm_call
+
+Layer 2: RunConfig(运行参数,每次 run 时指定)
+  ├─ 模型层:model, temperature, max_iterations, tools
+  └─ 框架层:trace_id, agent_type, uid, system_prompt, parent_trace_id, ...
+
+Layer 3: Messages(任务消息,OpenAI SDK 格式 List[Dict])
+  [{"role": "user", "content": "分析这张图的构图"}]
+```
+
+### RunConfig
+
+```python
+@dataclass
+class RunConfig:
+    # 模型层参数
+    model: str = "gpt-4o"
+    temperature: float = 0.3
+    max_iterations: int = 200
+    tools: Optional[List[str]] = None          # None = 全部已注册工具
+
+    # 框架层参数
+    agent_type: str = "default"
+    uid: Optional[str] = None
+    system_prompt: Optional[str] = None        # None = 从 skills 自动构建
+    skills: Optional[List[str]] = None         # 注入 system prompt 的 skill 名称列表;None = 按 preset 决定
+    enable_memory: bool = True
+    auto_execute_tools: bool = True
+    name: Optional[str] = None                 # 显示名称(空则由 utility_llm 自动生成)
+
+    # Trace 控制
+    trace_id: Optional[str] = None             # None = 新建
+    parent_trace_id: Optional[str] = None      # 子 Agent 专用
+    parent_goal_id: Optional[str] = None
+
+    # 续跑控制
+    after_sequence: Optional[int] = None       # 从哪条消息后续跑(message sequence)
+```
+
+**实现**:`agent/core/runner.py:RunConfig`
+
+### 三种运行模式
+
+通过 RunConfig 参数自然区分,统一入口 `run(messages, config)`:
+
+| 模式 | trace_id | after_sequence | messages 含义 | API 端点 |
+|------|----------|---------------|--------------|----------|
+| 新建 | None | - | 初始任务消息 | `POST /api/traces` |
+| 续跑 | 已有 ID | None 或 == head | 追加到末尾的新消息 | `POST /api/traces/{id}/run` |
+| 回溯 | 已有 ID | 主路径上 < head | 在插入点之后追加的新消息 | `POST /api/traces/{id}/run` |
+
+Runner 根据 `after_sequence` 与当前 `head_sequence` 的关系自动判断行为,前端无需指定模式。
+
+### 执行流程
+
+```python
+async def run(messages: List[Dict], config: RunConfig = None) -> AsyncIterator[Union[Trace, Message]]:
+    # Phase 1: PREPARE TRACE
+    #   无 trace_id → 创建新 Trace(生成 name,初始化 GoalTree)
+    #   有 trace_id + after_sequence 为 None 或 == head → 加载已有 Trace,状态置为 running
+    #   有 trace_id + after_sequence < head → 加载 Trace,执行 rewind(快照 GoalTree,重建,设 parent_sequence)
+    trace = await _prepare_trace(config)
+    yield trace
+
+    # Phase 2: BUILD HISTORY
+    #   从 head_sequence 沿 parent chain 回溯构建主路径消息
+    #   构建 system prompt(新建时注入 skills/experiences;续跑时复用已有)
+    #   追加 input messages(设置 parent_sequence 指向当前 head)
+    history, sequence = await _build_history(trace, messages, config)
+
+    # Phase 3: AGENT LOOP
+    for iteration in range(config.max_iterations):
+        # 周期性注入 GoalTree + Active Collaborators(每 10 轮)
+        if iteration % 10 == 0:
+            inject_context(goal_tree, collaborators)
+
+        response = await llm_call(messages=history, model=config.model, tools=tool_schemas)
+
+        # 按需自动创建 root goal(兜底)
+        # 记录 assistant Message
+        # 执行工具,记录 tool Messages
+        # 无 tool_calls 则 break
+
+    # Phase 4: COMPLETE
+    #   更新 Trace 状态 (completed/failed)
+    trace.status = "completed"
+    yield trace
+```
+
+**实现**:`agent/core/runner.py:AgentRunner`
+
+### 回溯(Rewind)
+
+回溯通过 `RunConfig(trace_id=..., after_sequence=N)` 触发(N 在主路径上且 < head_sequence),在 Phase 1 中执行:
+
+1. **验证插入点**:确保不截断在 assistant(tool_calls) 和 tool response 之间
+2. **快照 GoalTree**:将当前完整 GoalTree 存入 `events.jsonl`(rewind 事件的 `goal_tree_snapshot` 字段)
+3. **按时间重建 GoalTree**:以截断点消息的 `created_at` 为界,保留 `created_at <= cutoff_time` 的所有 goals(无论状态),丢弃 cutoff 之后创建的 goals,清空 `current_id`。将被保留的 `in_progress` goal 重置为 `pending`
+4. **设置 parent_sequence**:新消息的 `parent_sequence` 指向 rewind 点,旧消息自动脱离主路径
+5. **更新 Trace**:`head_sequence` 更新为新消息的 sequence,status 改回 running
+
+新消息的 sequence 从 `last_sequence + 1` 开始(全局递增,不复用)。旧消息无需标记 abandoned,通过消息树结构自然隔离。
+
+### 调用接口
+
+三种模式共享同一入口 `run(messages, config)`:
+
+```python
+# 新建
+async for item in runner.run(
+    messages=[{"role": "user", "content": "分析项目架构"}],
+    config=RunConfig(model="gpt-4o"),
+):
+    ...
+
+# 续跑:在已有 trace 末尾追加消息继续执行
+async for item in runner.run(
+    messages=[{"role": "user", "content": "继续"}],
+    config=RunConfig(trace_id="existing-trace-id"),
+):
+    ...
+
+# 回溯:从指定 sequence 处切断,插入新消息重新执行
+# after_sequence=5 表示新消息的 parent_sequence=5,从此处开始
+async for item in runner.run(
+    messages=[{"role": "user", "content": "换一个方案试试"}],
+    config=RunConfig(trace_id="existing-trace-id", after_sequence=5),
+):
+    ...
+
+# 重新生成:回溯后不插入新消息,直接基于已有消息重跑
+async for item in runner.run(
+    messages=[],
+    config=RunConfig(trace_id="existing-trace-id", after_sequence=5),
+):
+    ...
+```
+
+`after_sequence` 的值是 message 的 `sequence` 号,可通过 `GET /api/traces/{trace_id}/messages` 查看。如果指定的 sequence 是一条带 `tool_calls` 的 assistant 消息,系统会自动将截断点扩展到其所有对应的 tool response 之后(安全截断)。
+
+**停止运行**:
+
+```python
+# 停止正在运行的 Trace
+await runner.stop(trace_id)
+```
+
+调用后 agent loop 在下一个检查点退出,Trace 状态置为 `stopped`,同时保存当前 `head_sequence`(确保续跑时能正确加载完整历史)。
+
+**消息完整性保护(orphaned tool_call 修复)**:续跑加载历史时,`_build_history` 自动检测并修复 orphaned tool_calls(`_heal_orphaned_tool_calls`)。当 agent 被 stop/crash 中断时,可能存在 assistant 的 tool_calls 没有对应的 tool results(包括部分完成的情况:3 个 tool_call 只有 1 个 tool_result)。直接发给 LLM 会导致 400 错误。
+
+修复策略:为每个缺失的 tool_result **插入合成的中断通知**(而非裁剪 assistant 消息):
+
+| 工具类型 | 合成 tool_result 内容 |
+|----------|---------------------|
+| 普通工具 | 简短中断提示,建议重新调用 |
+| agent/evaluate | 结构化中断信息,包含 `sub_trace_id`、执行统计、`continue_from` 用法指引 |
+
+agent 工具的合成结果对齐正常返回值格式(含 `sub_trace_id` 字段),主 Agent 可直接使用 `agent(task=..., continue_from=sub_trace_id)` 续跑被中断的子 Agent。合成消息持久化存储,确保幂等。
+
+**实现**:`agent/core/runner.py:AgentRunner._heal_orphaned_tool_calls`
+
+- `run(messages, config)`:**核心方法**,流式返回 `AsyncIterator[Union[Trace, Message]]`
+- `run_result(messages, config, on_event=None)`:便利方法,内部消费 `run()`,返回结构化结果。`on_event` 回调可实时接收每个 Trace/Message 事件(用于调试时输出子 Agent 执行过程)。主要用于 `agent`/`evaluate` 工具内部
+
+### REST API
+
+#### 查询端点
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| GET  | `/api/traces` | 列出 Traces |
+| GET  | `/api/traces/{id}` | 获取 Trace 详情(含 GoalTree、Sub-Traces) |
+| GET  | `/api/traces/{id}/messages` | 获取 Messages(支持 mode=main_path/all) |
+| GET  | `/api/traces/running` | 列出正在运行的 Trace |
+| WS   | `/api/traces/{id}/watch` | 实时事件推送 |
+
+**实现**:`agent/trace/api.py`, `agent/trace/websocket.py`
+
+#### 控制端点
+
+需在 `api_server.py` 中配置 Runner。执行在后台异步进行,通过 WebSocket 监听进度。
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| POST | `/api/traces` | 新建 Trace 并执行 |
+| POST | `/api/traces/{id}/run` | 运行(统一续跑 + 回溯) |
+| POST | `/api/traces/{id}/stop` | 停止运行中的 Trace |
+| POST | `/api/traces/{id}/reflect` | 触发反思,从执行历史中提取经验 |
+| POST | `/api/traces/{id}/compact` | 触发压缩,通过侧分支多轮 agent 模式压缩上下文 |
+
+```bash
+# 新建
+curl -X POST http://localhost:8000/api/traces \
+  -H "Content-Type: application/json" \
+  -d '{"messages": [{"role": "user", "content": "分析项目架构"}], "model": "gpt-4o"}'
+
+# 续跑(after_sequence 为 null 或省略)
+curl -X POST http://localhost:8000/api/traces/{trace_id}/run \
+  -d '{"messages": [{"role": "user", "content": "继续深入分析"}]}'
+
+# 回溯:从 sequence 5 处截断,插入新消息重新执行
+curl -X POST http://localhost:8000/api/traces/{trace_id}/run \
+  -d '{"after_sequence": 5, "messages": [{"role": "user", "content": "换一个方案"}]}'
+
+# 重新生成:回溯到 sequence 5,不插入新消息,直接重跑
+curl -X POST http://localhost:8000/api/traces/{trace_id}/run \
+  -d '{"after_sequence": 5, "messages": []}'
+
+# 停止
+curl -X POST http://localhost:8000/api/traces/{trace_id}/stop
+
+# 反思:通过侧分支多轮 agent 模式提取经验
+curl -X POST http://localhost:8000/api/traces/{trace_id}/reflect \
+  -d '{"focus": "为什么第三步选择了错误的方案"}'
+
+# 压缩:通过侧分支多轮 agent 模式压缩上下文
+curl -X POST http://localhost:8000/api/traces/{trace_id}/compact
+```
+
+响应立即返回 `{"trace_id": "...", "status": "started"}`,通过 `WS /api/traces/{trace_id}/watch` 监听实时事件。
+
+**实现**:`agent/trace/run_api.py`
+
+#### 经验端点
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| GET  | `/api/experiences` | 读取经验文件内容 |
+
+**实现**:`agent/trace/run_api.py`
+
+---
+
+## 数据模型
+
+### Trace(任务执行)
+
+一次完整的 Agent 执行。所有 Agent(主、子、人类协助)都是 Trace。
+
+```python
+@dataclass
+class Trace:
+    trace_id: str
+    mode: Literal["call", "agent"]           # 单次调用 or Agent 模式
+
+    # Prompt 标识
+    prompt_name: Optional[str] = None
+
+    # Agent 模式特有
+    task: Optional[str] = None
+    agent_type: Optional[str] = None
+
+    # 父子关系(Sub-Trace 特有)
+    parent_trace_id: Optional[str] = None    # 父 Trace ID
+    parent_goal_id: Optional[str] = None     # 哪个 Goal 启动的
+
+    # 状态
+    status: Literal["running", "completed", "failed", "stopped"] = "running"
+
+    # 统计
+    total_messages: int = 0
+    total_tokens: int = 0                    # 总 tokens(prompt + completion)
+    total_prompt_tokens: int = 0
+    total_completion_tokens: int = 0
+    total_cost: float = 0.0
+    total_duration_ms: int = 0
+
+    # 进度追踪
+    last_sequence: int = 0                   # 最新 message 的 sequence(全局递增,不复用)
+    head_sequence: int = 0                   # 当前主路径的头节点 sequence(用于 build_llm_messages)
+    last_event_id: int = 0                   # 最新事件 ID(用于 WS 续传)
+
+    # 配置
+    uid: Optional[str] = None
+    model: Optional[str] = None              # 默认模型
+    tools: Optional[List[Dict]] = None       # 工具定义(OpenAI 格式)
+    llm_params: Dict[str, Any] = {}          # LLM 参数(temperature 等)
+    context: Dict[str, Any] = {}             # 元数据(含 collaborators 列表)
+
+    # 当前焦点
+    current_goal_id: Optional[str] = None
+
+    # 结果
+    result_summary: Optional[str] = None
+    error_message: Optional[str] = None
+
+    # 时间
+    created_at: datetime
+    completed_at: Optional[datetime] = None
+```
+
+**实现**:`agent/trace/models.py`
+
+### Goal(目标节点)
+
+计划中的一个目标,支持层级结构。单独存储于 `goal.json`。
+
+```python
+@dataclass
+class Goal:
+    id: str                                  # 内部 ID("1", "2"...)
+    description: str
+    reason: str = ""                         # 创建理由
+    parent_id: Optional[str] = None          # 父 Goal ID
+    type: GoalType = "normal"                # normal | agent_call
+    status: GoalStatus = "pending"           # pending | in_progress | completed | abandoned
+    summary: Optional[str] = None            # 完成/放弃时的总结
+
+    # agent_call 特有(启动 Sub-Trace)
+    sub_trace_ids: Optional[List[str]] = None
+    agent_call_mode: Optional[str] = None    # explore | delegate | evaluate
+    sub_trace_metadata: Optional[Dict] = None
+
+    # 统计
+    self_stats: GoalStats                    # 自身 Messages 统计
+    cumulative_stats: GoalStats              # 包含子孙的累计统计
+
+    created_at: datetime
+```
+
+**Goal 类型**:
+- `normal` - 普通目标,由 Agent 直接执行
+- `agent_call` - 通过 `agent`/`evaluate` 工具创建的目标,会启动 Sub-Trace
+
+**agent_call 类型的 Goal**:
+- 调用 `agent`/`evaluate` 工具时自动设置
+- `agent_call_mode` 记录使用的模式(explore/delegate/evaluate)
+- `sub_trace_ids` 记录创建的所有 Sub-Trace ID
+- 状态转换:pending → in_progress(Sub-Trace 启动)→ completed(Sub-Trace 完成)
+- `summary` 包含格式化的汇总结果(explore 模式会汇总所有分支)
+
+**Goal 操作**(通过 goal 工具):
+- `add` - 添加顶层目标
+- `under` - 在指定目标下添加子目标
+- `after` - 在指定目标后添加兄弟目标
+- `focus` - 切换焦点到指定目标
+- `done` - 完成当前目标(附带 summary)
+- `abandon` - 放弃当前目标(附带原因)
+
+**实现**:`agent/trace/goal_models.py`, `agent/trace/goal_tool.py`
+
+### Message(执行消息)
+
+对应 LLM API 的消息,每条 Message 关联一个 Goal。消息通过 `parent_sequence` 形成树结构。
+
+```python
+@dataclass
+class Message:
+    message_id: str                          # 格式:{trace_id}-{sequence:04d}
+    trace_id: str
+    role: Literal["system", "user", "assistant", "tool"]
+    sequence: int                            # 全局顺序(递增,不复用)
+    parent_sequence: Optional[int] = None    # 父消息的 sequence(构成消息树)
+    goal_id: Optional[str] = None            # 关联的 Goal ID(初始消息为 None,系统会按需自动创建 root goal 兜底)
+    description: str = ""                    # 系统自动生成的摘要
+    tool_call_id: Optional[str] = None
+    content: Any = None
+
+    # 统计
+    prompt_tokens: Optional[int] = None
+    completion_tokens: Optional[int] = None
+    cost: Optional[float] = None
+    duration_ms: Optional[int] = None
+
+    # LLM 响应信息(仅 role="assistant")
+    finish_reason: Optional[str] = None
+
+    created_at: datetime
+
+    # [已弃用] 由 parent_sequence 树结构替代
+    status: Literal["active", "abandoned"] = "active"
+    abandoned_at: Optional[datetime] = None
+```
+
+**消息树(Message Tree)**:
+
+消息通过 `parent_sequence` 形成树。主路径 = 从 `trace.head_sequence` 沿 parent chain 回溯到 root。
+
+```
+正常对话:1 → 2 → 3 → 4 → 5       (每条的 parent 指向前一条)
+Rewind 到 3:3 → 6(parent=3) → 7   (新主路径,4-5 自动脱离)
+压缩 1-3:   8(summary, parent=None) → 6 → 7  (summary 跳过被压缩的消息)
+侧分支:     5 → 6(branch_type="compression", parent=5) → 7(parent=6)
+            5 → 8(summary, parent=5, 主路径)
+            (侧分支消息 6-7 通过 parent_sequence 自然脱离主路径)
+```
+
+`build_llm_messages` = 从 `trace.head_sequence` 沿 parent_sequence 链回溯到 root,反转后返回。
+
+**关键设计**:只要 `trace.head_sequence` 管理正确(始终指向主路径),`get_main_path_messages()` 自然返回主路径消息,侧分支消息通过 parent_sequence 链自动被跳过,无需额外过滤。
+
+Message 提供格式转换方法:
+- `to_llm_dict()` → OpenAI 格式 Dict(用于 LLM 调用)
+- `from_llm_dict(d, trace_id, sequence, goal_id)` → 从 OpenAI 格式创建 Message
+
+**侧分支字段**:
+- `branch_type`: "compression" | "reflection" | None(主路径)
+- `branch_id`: 同一侧分支的消息共享 branch_id
+
+**实现**:`agent/trace/models.py`
+
+---
+
+## Agent 预设
+
+不同类型 Agent 的配置模板,控制工具权限和参数。
+
+```python
+@dataclass
+class AgentPreset:
+    allowed_tools: Optional[List[str]] = None  # None 表示允许全部
+    denied_tools: Optional[List[str]] = None   # 黑名单
+    max_iterations: int = 30
+    temperature: Optional[float] = None
+    skills: Optional[List[str]] = None         # 注入 system prompt 的 skill 名称列表;None = 加载全部
+    description: Optional[str] = None
+
+
+_DEFAULT_SKILLS = ["planning", "research", "browser"]
+
+AGENT_PRESETS = {
+    "default": AgentPreset(
+        allowed_tools=None,
+        max_iterations=30,
+        skills=_DEFAULT_SKILLS,
+        description="默认 Agent,拥有全部工具权限",
+    ),
+    "explore": AgentPreset(
+        allowed_tools=["read", "glob", "grep", "list_files"],
+        denied_tools=["write", "edit", "bash", "task"],
+        max_iterations=15,
+        skills=["planning"],
+        description="探索型 Agent,只读权限,用于代码分析",
+    ),
+    "analyst": AgentPreset(
+        allowed_tools=["read", "glob", "grep", "web_search", "webfetch"],
+        denied_tools=["write", "edit", "bash", "task"],
+        temperature=0.3,
+        max_iterations=25,
+        skills=["planning", "research"],
+        description="分析型 Agent,用于深度分析和研究",
+    ),
+}
+```
+
+**实现**:`agent/core/presets.py`
+
+**用户自定义**:项目级配置文件(如 `examples/how/presets.json`)可通过 `register_preset()` 注册额外预设。项目专用的 Agent 类型建议放在项目目录下,而非内置预设。
+
+---
+
+## 子 Trace 机制
+
+通过 `agent` 工具创建子 Agent 执行任务。`task` 参数为字符串时为单任务(delegate),为列表时并行执行多任务(explore)。支持通过 `messages` 参数预置消息,通过 `continue_from` 参数续跑已有 Sub-Trace。
+
+`agent` 工具负责创建 Sub-Trace 和初始化 GoalTree(因为需要设置自定义 context 元数据和命名规则),创建完成后将 `trace_id` 传给 `RunConfig`,由 Runner 接管后续执行。工具同时维护父 Trace 的 `context["collaborators"]` 列表。
+
+### 跨设备 Agent 通信
+
+支持跨设备的 Agent 间持续对话,通过远程 Trace ID 实现:
+
+**Trace ID 格式**:
+- 本地 Trace:`abc-123`
+- 远程 Trace:`agent://terminal-agent-456/abc-123`(协议 + Agent 地址 + 本地 ID)
+
+**使用方式**:
+```python
+# 调用远程 Agent
+result = agent(task="分析本地项目", agent_url="https://terminal-agent.local")
+# 返回: {"sub_trace_id": "agent://terminal-agent.local/abc-123"}
+
+# 续跑远程 Trace(持续对话)
+result2 = agent(
+    task="重点分析core模块",
+    continue_from="agent://terminal-agent.local/abc-123",
+    agent_url="https://terminal-agent.local"
+)
+```
+
+**实现**:`HybridTraceStore` 自动路由到本地或远程存储,远程访问通过 HTTP API 实现。
+
+**实现位置**:`agent/trace/hybrid_store.py`(规划中)
+
+### agent 工具
+
+```python
+@tool(description="创建 Agent 执行任务")
+async def agent(
+    task: Union[str, List[str]],
+    messages: Optional[Union[Messages, List[Messages]]] = None,
+    continue_from: Optional[str] = None,
+    agent_type: Optional[str] = None,
+    skills: Optional[List[str]] = None,
+    agent_url: Optional[str] = None,  # 远程 Agent 地址(跨设备)
+    context: Optional[dict] = None,
+) -> Dict[str, Any]:
+```
+
+**参数**:
+- `agent_type`: 子 Agent 类型,决定工具权限和默认 skills(对应 `AgentPreset` 名称)
+- `skills`: 覆盖 preset 默认值,显式指定注入 system prompt 的 skill 列表
+- `agent_url`: 远程 Agent 地址,用于跨设备调用(返回远程 Trace ID)
+- `continue_from`: 支持本地或远程 Trace ID
+
+**单任务(delegate)**:`task: str`
+- 创建单个 Sub-Trace
+- 完整工具权限(除 agent/evaluate 外,防止递归)
+- 支持 `continue_from` 续跑已有 Sub-Trace(本地或远程)
+- 支持 `messages` 预置上下文消息
+
+**多任务(explore)**:`task: List[str]`
+- 使用 `asyncio.gather()` 并行执行所有任务
+- 每个任务创建独立的 Sub-Trace
+- 只读工具权限(read_file, grep_content, glob_files, goal)
+- `messages` 支持 1D(共享)或 2D(per-agent)
+- 不支持 `continue_from`
+- 汇总所有分支结果返回
+
+### evaluate 工具
+
+```python
+@tool(description="评估目标执行结果是否满足要求")
+async def evaluate(
+    messages: Optional[Messages] = None,
+    target_goal_id: Optional[str] = None,
+    continue_from: Optional[str] = None,
+    context: Optional[dict] = None,
+) -> Dict[str, Any]:
+```
+
+- 代码自动从 GoalTree 注入目标描述(无需 criteria 参数)
+- 模型把执行结果和上下文放在 `messages` 中
+- `target_goal_id` 默认为当前 goal_id
+- 只读工具权限
+- 返回评估结论和改进建议
+
+### 消息类型别名
+
+定义在 `agent/trace/models.py`,用于工具参数和 runner/LLM API 接口:
+
+```python
+ChatMessage = Dict[str, Any]                          # 单条 OpenAI 格式消息
+Messages = List[ChatMessage]                          # 消息列表
+MessageContent = Union[str, List[Dict[str, str]]]     # content 字段(文本或多模态)
+```
+
+**实现位置**:`agent/tools/builtin/subagent.py`
+
+**详细文档**:[工具系统 - Agent/Evaluate 工具](../agent/docs/tools.md#agent-工具)
+
+### ask_human 工具
+
+创建阻塞式 Trace,等待人类通过 IM/邮件等渠道回复。
+
+**注意**:此功能规划中,暂未实现。
+
+---
+
+## Active Collaborators(活跃协作者)
+
+任务执行中与模型密切协作的实体(子 Agent 或人类),按 **与当前任务的关系** 分类,而非按 human/agent 分类:
+
+| | 持久存在(外部可查) | 任务内活跃(需要注入) |
+|---|---|---|
+| Agent | 专用 Agent(代码审查等) | 当前任务创建的子 Agent |
+| Human | 飞书通讯录 | 当前任务中正在对接的人 |
+
+### 数据模型
+
+活跃协作者存储在 `trace.context["collaborators"]`:
+
+```python
+{
+    "name": "researcher",            # 名称(模型可见)
+    "type": "agent",                 # agent | human
+    "trace_id": "abc-@delegate-001", # trace_id(agent 场景)
+    "status": "completed",           # running | waiting | completed | failed
+    "summary": "方案A最优",          # 最近状态摘要
+}
+```
+
+### 注入方式
+
+与 GoalTree 一同周期性注入(每 10 轮),渲染为 Markdown:
+
+```markdown
+## Active Collaborators
+- researcher [agent, completed]: 方案A最优
+- 谭景玉 [human, waiting]: 已发送方案确认,等待回复
+- coder [agent, running]: 正在实现特征提取模块
+```
+
+列表为空时不注入。
+
+### 维护
+
+各工具负责更新 collaborators 列表(通过 `context["store"]` 写入 trace.context):
+- `agent` 工具:创建/续跑子 Agent 时更新
+- `feishu` 工具:发送消息/收到回复时更新
+- Runner 只负责读取和注入
+
+**持久联系人/Agent**:通过工具按需查询(如 `feishu_get_contact_list`),不随任务注入。
+
+**实现**:`agent/core/runner.py:AgentRunner._build_context_injection`, `agent/tools/builtin/subagent.py`
+
+---
+
+## Context Injection Hooks(上下文注入钩子)
+
+### 概述
+
+Context Injection Hooks 是一个可扩展机制,允许外部模块(如 A2A IM、监控系统)向 Agent 的周期性上下文注入中添加自定义内容。
+
+### 设计理念
+
+- **周期性注入**:每 10 轮自动注入,不打断执行
+- **可扩展**:通过 hook 函数注册,无需修改 Runner 核心代码
+- **轻量提醒**:只注入摘要/提醒,详细内容通过工具获取
+- **LLM 自主决策**:由 LLM 决定何时响应提醒
+
+### 架构
+
+```
+Runner Loop (每 10 轮)
+    ↓
+_build_context_injection()
+    ├─ GoalTree (内置)
+    ├─ Active Collaborators (内置)
+    └─ Context Hooks (可扩展)
+         ├─ A2A IM Hook → "💬 3 条新消息"
+         ├─ Monitor Hook → "⚠️ 内存使用 85%"
+         └─ Custom Hook → 自定义内容
+    ↓
+注入为 system message
+    ↓
+LLM 看到提醒 → 决定是否调用工具
+```
+
+### Hook 接口
+
+```python
+# Hook 函数签名
+def context_hook(trace: Trace, goal_tree: Optional[GoalTree]) -> Optional[str]:
+    """
+    生成要注入的上下文内容
+
+    Args:
+        trace: 当前 Trace
+        goal_tree: 当前 GoalTree
+
+    Returns:
+        要注入的 Markdown 内容,None 表示无内容
+    """
+    return "## Custom Section\n\n内容..."
+```
+
+### 注册 Hook
+
+```python
+# 创建 Runner 时注册
+runner = AgentRunner(
+    llm_call=llm_call,
+    trace_store=trace_store,
+    context_hooks=[hook1, hook2, hook3]  # 按顺序注入
+)
+```
+
+### 实现
+
+**Runner 修改**:
+
+```python
+# agent/core/runner.py
+
+class AgentRunner:
+    def __init__(
+        self,
+        # ... 现有参数
+        context_hooks: Optional[List[Callable]] = None
+    ):
+        self.context_hooks = context_hooks or []
+
+    def _build_context_injection(
+        self,
+        trace: Trace,
+        goal_tree: Optional[GoalTree],
+    ) -> str:
+        """构建周期性注入的上下文(GoalTree + Active Collaborators + Hooks)"""
+        parts = []
+
+        # GoalTree(现有)
+        if goal_tree and goal_tree.goals:
+            parts.append(f"## Current Plan\n\n{goal_tree.to_prompt()}")
+            # ... focus 提醒
+
+        # Active Collaborators(现有)
+        collaborators = trace.context.get("collaborators", [])
+        if collaborators:
+            lines = ["## Active Collaborators"]
+            for c in collaborators:
+                # ... 现有逻辑
+            parts.append("\n".join(lines))
+
+        # Context Hooks(新增)
+        for hook in self.context_hooks:
+            try:
+                hook_content = hook(trace, goal_tree)
+                if hook_content:
+                    parts.append(hook_content)
+            except Exception as e:
+                logger.error(f"Context hook error: {e}")
+
+        return "\n\n".join(parts)
+```
+
+**实现位置**:`agent/core/runner.py:AgentRunner._build_context_injection`(待实现)
+
+### 示例:A2A IM Hook
+
+```python
+# agent/tools/builtin/a2a_im.py
+
+class A2AMessageQueue:
+    """A2A IM 消息队列"""
+
+    def __init__(self):
+        self._messages: List[Dict] = []
+
+    def push(self, message: Dict):
+        """Gateway 推送消息时调用"""
+        self._messages.append(message)
+
+    def pop_all(self) -> List[Dict]:
+        """check_messages 工具调用时清空"""
+        messages = self._messages
+        self._messages = []
+        return messages
+
+    def get_summary(self) -> Optional[str]:
+        """获取消息摘要(用于 context injection)"""
+        if not self._messages:
+            return None
+
+        count = len(self._messages)
+        latest = self._messages[-1]
+        from_agent = latest.get("from_agent_id", "unknown")
+
+        if count == 1:
+            return f"💬 来自 {from_agent} 的 1 条新消息(使用 check_messages 工具查看)"
+        else:
+            return f"💬 {count} 条新消息,最新来自 {from_agent}(使用 check_messages 工具查看)"
+
+
+def create_a2a_context_hook(message_queue: A2AMessageQueue):
+    """创建 A2A IM 的 context hook"""
+
+    def a2a_context_hook(trace: Trace, goal_tree: Optional[GoalTree]) -> Optional[str]:
+        """注入 A2A IM 消息提醒"""
+        summary = message_queue.get_summary()
+        if not summary:
+            return None
+
+        return f"## Messages\n\n{summary}"
+
+    return a2a_context_hook
+
+
+@tool(description="检查来自其他 Agent 的新消息")
+async def check_messages(ctx: ToolContext) -> ToolResult:
+    """检查并获取来自其他 Agent 的新消息"""
+    message_queue: A2AMessageQueue = ctx.context.get("a2a_message_queue")
+    if not message_queue:
+        return ToolResult(title="消息队列未初始化", output="")
+
+    messages = message_queue.pop_all()
+
+    if not messages:
+        return ToolResult(title="无新消息", output="")
+
+    # 格式化消息
+    lines = [f"收到 {len(messages)} 条新消息:\n"]
+    for i, msg in enumerate(messages, 1):
+        from_agent = msg.get("from_agent_id", "unknown")
+        content = msg.get("content", "")
+        conv_id = msg.get("conversation_id", "")
+        lines.append(f"{i}. 来自 {from_agent}")
+        lines.append(f"   对话 ID: {conv_id}")
+        lines.append(f"   内容: {content}")
+        lines.append("")
+
+    return ToolResult(
+        title=f"收到 {len(messages)} 条新消息",
+        output="\n".join(lines)
+    )
+```
+
+**实现位置**:`agent/tools/builtin/a2a_im.py`(待实现)
+
+### 配置示例
+
+```python
+# api_server.py
+
+from agent.tools.builtin.a2a_im import (
+    A2AMessageQueue,
+    create_a2a_context_hook,
+    check_messages
+)
+
+# 创建消息队列
+message_queue = A2AMessageQueue()
+
+# 创建 context hook
+a2a_hook = create_a2a_context_hook(message_queue)
+
+# 创建 Runner 时注入 hook
+runner = AgentRunner(
+    llm_call=llm_call,
+    trace_store=trace_store,
+    context_hooks=[a2a_hook]
+)
+
+# 注册 check_messages 工具
+tool_registry.register(check_messages)
+
+# 启动 Gateway webhook 端点
+@app.post("/webhook/a2a-messages")
+async def receive_a2a_message(message: dict):
+    """接收来自 Gateway 的消息"""
+    message_queue.push(message)
+    return {"status": "received"}
+```
+
+### 注入效果
+
+```markdown
+## Current Plan
+1. [in_progress] 分析代码架构
+   1.1. [completed] 读取项目结构
+   1.2. [in_progress] 分析核心模块
+
+## Active Collaborators
+- researcher [agent, completed]: 已完成调研
+
+## Messages
+💬 来自 code-reviewer 的 1 条新消息(使用 check_messages 工具查看)
+```
+
+### 其他应用场景
+
+**监控告警**:
+
+```python
+def create_monitor_hook(monitor):
+    def monitor_hook(trace, goal_tree):
+        alerts = monitor.get_alerts()
+        if not alerts:
+            return None
+        return f"## System Alerts\n\n⚠️ {len(alerts)} 条告警(使用 check_alerts 工具查看)"
+    return monitor_hook
+```
+
+**定时提醒**:
+
+```python
+def create_timer_hook(timer):
+    def timer_hook(trace, goal_tree):
+        if timer.should_remind():
+            return "## Reminder\n\n⏰ 任务已执行 30 分钟,建议检查进度"
+        return None
+    return timer_hook
+```
+
+**实现位置**:各模块自行实现 hook 函数
+
+---
+
+## Active Collaborators(活跃协作者)
+
+任务执行中与模型密切协作的实体(子 Agent 或人类),按 **与当前任务的关系** 分类,而非按 human/agent 分类:
+
+| | 持久存在(外部可查) | 任务内活跃(需要注入) |
+|---|---|---|
+| Agent | 专用 Agent(代码审查等) | 当前任务创建的子 Agent |
+| Human | 飞书通讯录 | 当前任务中正在对接的人 |
+
+### 数据模型
+
+活跃协作者存储在 `trace.context["collaborators"]`:
+
+```python
+{
+    "name": "researcher",            # 名称(模型可见)
+    "type": "agent",                 # agent | human
+    "trace_id": "abc-@delegate-001", # trace_id(agent 场景)
+    "status": "completed",           # running | waiting | completed | failed
+    "summary": "方案A最优",          # 最近状态摘要
+}
+```
+
+### 注入方式
+
+与 GoalTree 一同周期性注入(每 10 轮),渲染为 Markdown:
+
+```markdown
+## Active Collaborators
+- researcher [agent, completed]: 方案A最优
+- 谭景玉 [human, waiting]: 已发送方案确认,等待回复
+- coder [agent, running]: 正在实现特征提取模块
+```
+
+列表为空时不注入。
+
+### 维护
+
+各工具负责更新 collaborators 列表(通过 `context["store"]` 写入 trace.context):
+- `agent` 工具:创建/续跑子 Agent 时更新
+- `feishu` 工具:发送消息/收到回复时更新
+- Runner 只负责读取和注入
+
+**持久联系人/Agent**:通过工具按需查询(如 `feishu_get_contact_list`),不随任务注入。
+
+**实现**:`agent/core/runner.py:AgentRunner._build_context_injection`, `agent/tools/builtin/subagent.py`
+
+---
+
+## 工具系统
+
+### 核心概念
+
+```python
+@tool()
+async def my_tool(arg: str, ctx: ToolContext) -> ToolResult:
+    return ToolResult(
+        title="Success",
+        output="Result content",
+        long_term_memory="Short summary"  # 可选:压缩后保留的摘要
+    )
+```
+
+| 类型 | 作用 |
+|------|------|
+| `@tool` | 装饰器,自动注册工具并生成 Schema |
+| `ToolResult` | 工具执行结果,支持双层记忆 |
+| `ToolContext` | 工具执行上下文,依赖注入 |
+
+### 工具分类
+
+| 目录 | 工具 | 说明 |
+|-----|------|------|
+| `trace/` | goal | Agent 内部计划管理 |
+| `builtin/` | agent, evaluate | 子 Agent 创建与评估 |
+| `builtin/file/` | read, write, edit, glob, grep | 文件操作 |
+| `builtin/browser/` | browser actions | 浏览器自动化 |
+| `builtin/` | bash, sandbox, search, webfetch, skill, ask_human | 其他工具 |
+
+### 双层记忆管理
+
+大输出(如网页抓取)只传给 LLM 一次,之后用摘要替代:
+
+```python
+ToolResult(
+    output="<10K tokens 的完整内容>",
+    long_term_memory="Extracted 10000 chars from amazon.com",
+    include_output_only_once=True
+)
+```
+
+**详细文档**:[工具系统](../agent/docs/tools.md)
+
+---
+
+## Skills 系统
+
+### 分类
+
+| 类型 | 加载位置 | 加载时机 |
+|------|---------|---------|
+| **内置 Skill** | System Prompt | Agent 启动时自动注入 |
+| **项目 Skill** | System Prompt | Agent 启动时按 preset/call-site 过滤后注入 |
+| **普通 Skill** | 对话消息 | 模型调用 `skill` 工具时 |
+
+### 目录结构
+
+```
+agent/skill/skills/         # 内置 Skills(始终加载)
+├── planning.md              # 计划与 Goal 工具使用
+├── research.md              # 搜索与内容研究
+└── browser.md               # 浏览器自动化
+
+./skills/                    # 项目自定义 Skills
+```
+
+### Skills 过滤(call-site 选择)
+
+不同 Agent 类型所需的 skills 不同。过滤优先级:
+
+1. `agent()` 工具的 `skills` 参数(显式指定,最高优先级)
+2. `AgentPreset.skills`(preset 默认值)
+3. `None`(加载全部,向后兼容)
+
+示例:调用子 Agent 时只注入解构相关 skill:
+```python
+agent(task="...", agent_type="deconstruct", skills=["planning", "deconstruct"])
+```
+
+**实现**:`agent/skill/skill_loader.py`
+
+**详细文档**:[Skills 使用指南](../agent/docs/skills.md)
+
+---
+
+## Experiences 系统
+
+从执行历史中提取的经验规则,用于指导未来任务。
+
+### 存储规范
+
+经验以 Markdown 文件存储(默认 `./.cache/experiences.md`),人类可读、可编辑、可版本控制。
+
+文件格式:
+
+```markdown
+---
+id: ex_001
+trace_id: trace-xxx
+category: tool_usage
+tags: {state: ["large_file", "dirty_repo"], intent: ["batch_edit", "safe_modify"]}
+metrics: {helpful: 12, harmful: 0}
+created_at: 2026-02-12 15:30
+---
+
+---
+id: ex_002
+...
+```
+---
+
+
+### 反思机制(Reflect)
+
+通过 POST /api/traces/{id}/reflect 触发,旨在将原始执行历史提炼为可复用的知识。
+    1. 分叉反思:在 trace 末尾追加 user message(含反思与打标 Prompt),作为侧枝执行。
+    2. 结构化生成:
+        ·归类:将经验分配至 tool_usage(工具)、logic_flow(逻辑)、environment(环境)等。
+        ·打标:提取 state(环境状态)与 intent(用户意图)语义标签。
+        ·量化:初始 helpful 设为 1。
+    3. 持久化:将带有元数据的 Markdown 块追加至 experiences.md。
+
+实现:agent/trace/run_api.py:reflect_trace
+
+### 语义注入与匹配流程
+新建 Trace 时,Runner 采用“分析-检索-注入”三阶段策略,实现精准经验推荐。
+    1. 意图预分析
+    Runner 调用 utility_llm 对初始任务进行语义提取:
+        -输入:"优化这个项目的 Docker 构建速度"
+        -输出:{state: ["docker", "ci"], intent: ["optimization"]}
+    2. 语义检索
+        在 _load_experiences 中根据标签进行语义匹配(优先匹配 intent,其次是 state),筛选出相关度最高的 Top-K 条经验。
+    3. 精准注入
+        将匹配到的经验注入第一条 user message 末尾:
+```python
+# _build_history 中(仅新建模式):
+if not config.trace_id:
+    relevant_ex = self.experience_retriever.search(task_tags)
+    if relevant_ex:
+        formatted_ex = "\n".join([f"- [{e.id}] {e.content} (Helpful: {e.helpful})" for e in relevant_ex])
+        first_user_msg["content"] += f"\n\n## 参考经验\n\n{formatted_ex}"
+```
+实现:agent/core/runner.py:AgentRunner._build_history
+
+### 经验获取工具
+不再仅限于启动时自动注入,而是通过内置工具供 Agent 在需要时主动调用。当执行结果不符合预期或进入未知领域时,Agent 应优先使用此工具。
+工具定义:
+
+```python
+@tool(description="根据当前任务状态和意图,从经验库中检索相关的历史经验")
+async def get_experience(
+    intent: Optional[str] = None, 
+    state: Optional[str] = None
+) -> Dict[str, Any]:
+    """
+    参数:
+        intent: 想要达成的目标意图 (如 "optimization", "debug")
+        state: 当前环境或遇到的问题状态 (如 "docker_build_fail", "permission_denied")
+    """
+```
+实现: agent/tools/builtin/experience.py
+
+- 语义匹配与应用流程
+    当 Agent 调用 get_experience 时,系统执行以下逻辑:
+    1. 语义检索:根据传入的 intent 或 state 标签,在 experiences.md 中进行匹配。匹配权重:intent > state > helpful 评分。
+    2. 动态注入:工具返回匹配到的 Top-K 条经验(含 ID 和内容)。
+    3. 策略应用:Agent 接收到工具返回的经验后,需在后续 thought 中声明所选用的策略 ID(如 [ex_001]),并据此调整 goal_tree 或工具调用序列。
+
+## Context 压缩
+
+### 两级压缩策略
+
+#### Level 1:GoalTree 过滤(确定性,零成本)
+
+每轮 agent loop 构建 `llm_messages` 时自动执行:
+- 始终保留:system prompt、第一条 user message(含 GoalTree 精简视图)、当前 focus goal 的消息
+- 跳过 completed/abandoned goals 的消息(信息已在 GoalTree summary 中)
+- 通过 Message Tree 的 parent_sequence 实现跳过
+
+大多数情况下 Level 1 足够。
+
+#### Level 2:LLM 总结(仅在 Level 1 后仍超限时触发)
+
+触发条件:Level 1 之后 token 数仍超过阈值(默认 `max_tokens × 0.8`)。
+
+流程:
+1. **经验提取**:在消息列表末尾追加反思 prompt,进入侧分支 agent 模式(最多 5 轮),LLM 可调用工具(如 knowledge_search, knowledge_save)进行多轮推理。反思消息标记为 `branch_type="reflection"`,不在主路径上
+2. **压缩**:在消息列表末尾追加压缩 prompt(含 GoalTree 完整视图),进入侧分支 agent 模式(最多 5 轮),LLM 可调用工具(如 goal_status)辅助压缩。压缩消息标记为 `branch_type="compression"`,完成后创建 summary 消息,其 `parent_sequence` 跳过被压缩的范围
+
+**侧分支模式**:压缩和反思在同一 agent loop 中通过状态机实现,复用主路径的缓存和工具配置,支持多轮推理。
+
+### GoalTree 双视图
+
+`to_prompt()` 支持两种模式:
+- `include_summary=False`(默认):精简视图,用于日常周期性注入
+- `include_summary=True`:含所有 completed goals 的 summary,用于 Level 2 压缩时提供上下文
+
+### 压缩存储
+
+- 原始消息永远保留在 `messages/`
+- 压缩 summary 作为普通 Message 存储
+- 侧分支消息通过 `branch_type` 和 `branch_id` 标记,查询主路径时自动过滤
+- 通过 `parent_sequence` 树结构实现跳过,无需 compression events 或 skip list
+- Rewind 到压缩区域内时,summary 脱离主路径,原始消息自动恢复
+
+**实现**:`agent/core/runner.py:_agent_loop`, `agent/trace/compaction.py`, `agent/trace/goal_models.py`
+
+---
+
+## 存储接口
+
+```python
+class TraceStore(Protocol):
+    async def create_trace(self, trace: Trace) -> None: ...
+    async def get_trace(self, trace_id: str) -> Trace: ...
+    async def update_trace(self, trace_id: str, **updates) -> None: ...
+    async def add_message(self, message: Message) -> None: ...
+    async def get_trace_messages(self, trace_id: str) -> List[Message]: ...
+    async def get_main_path_messages(self, trace_id: str, head_sequence: int) -> List[Message]: ...
+    async def get_messages_by_goal(self, trace_id: str, goal_id: str) -> List[Message]: ...
+    async def append_event(self, trace_id: str, event_type: str, payload: Dict) -> int: ...
+```
+
+`get_main_path_messages` 从 `head_sequence` 沿 `parent_sequence` 链回溯,返回主路径上的有序消息列表。
+
+**实现**:
+- 协议定义:`agent/trace/protocols.py`
+- 本地存储:`agent/trace/store.py:FileSystemTraceStore`
+- 远程存储:`agent/trace/remote_store.py:RemoteTraceStore`(规划中)
+- 混合存储:`agent/trace/hybrid_store.py:HybridTraceStore`(规划中)
+
+### 跨设备存储
+
+**HybridTraceStore** 根据 Trace ID 自动路由到本地或远程存储:
+
+| Trace ID 格式 | 存储位置 | 访问方式 |
+|--------------|---------|---------|
+| `abc-123` | 本地文件系统 | `FileSystemTraceStore` |
+| `agent://host/abc-123` | 远程 Agent | HTTP API(`RemoteTraceStore`) |
+
+**RemoteTraceStore** 通过 HTTP API 访问远程 Trace:
+- `GET /api/traces/{trace_id}` - 获取 Trace 元数据
+- `GET /api/traces/{trace_id}/messages` - 获取消息历史
+- `POST /api/traces/{trace_id}/run` - 续跑(追加消息并执行)
+
+**认证**:通过 API Key 认证,配置在 `config/agents.yaml`。
+
+**实现位置**:`agent/trace/hybrid_store.py`, `agent/trace/remote_store.py`(规划中)
+
+### 存储结构
+
+```
+.trace/
+├── {trace_id}/
+│   ├── meta.json        # Trace 元数据(含 tools 定义)
+│   ├── goal.json        # GoalTree(mission + goals 列表)
+│   ├── events.jsonl     # 事件流(goal 变更、sub_trace 生命周期等)
+│   └── messages/        # Messages
+│       ├── {trace_id}-0001.json
+│       └── ...
+│
+└── {trace_id}@explore-{序号}-{timestamp}-001/  # 子 Trace
+    └── ...
+```
+
+**events.jsonl 说明**:
+- 记录 Trace 执行过程中的关键事件
+- 每行一个 JSON 对象,包含 event_id、event 类型、时间戳等
+- 主要事件类型:goal_added, goal_updated, sub_trace_started, sub_trace_completed, rewind
+- 用于实时监控和历史回放
+
+**Sub-Trace 目录命名**:
+- Explore: `{parent}@explore-{序号:03d}-{timestamp}-001`
+- Delegate: `{parent}@delegate-{timestamp}-001`
+- Evaluate: `{parent}@evaluate-{timestamp}-001`
+
+**meta.json 示例**:
+```json
+{
+  "trace_id": "0415dc38-...",
+  "mode": "agent",
+  "task": "分析代码结构",
+  "agent_type": "default",
+  "status": "running",
+  "model": "google/gemini-2.5-flash",
+  "tools": [...],
+  "llm_params": {"temperature": 0.3},
+  "context": {
+    "collaborators": [
+      {"name": "researcher", "type": "agent", "trace_id": "...", "status": "completed", "summary": "方案A最优"}
+    ]
+  },
+  "current_goal_id": "3"
+}
+```
+
+---
+
+## 设计决策
+
+详见 [设计决策文档](./decisions.md)
+
+**核心决策**:
+
+1. **所有 Agent 都是 Trace** - 主 Agent、子 Agent、人类协助统一为 Trace,通过 `parent_trace_id` 和 `spawn_tool` 区分
+
+2. **trace/ 模块统一管理执行状态** - 合并原 execution/ 和 goal/,包含计划管理和 Agent 内部控制工具
+
+3. **tools/ 专注外部交互** - 文件、命令、网络、浏览器等与外部世界的交互
+
+4. **Agent 预设替代 Sub-Agent 配置** - 通过 `core/presets.py` 定义不同类型 Agent 的工具权限和参数
+
+---
+
+## 相关文档
+
+| 文档 | 内容 |
+|-----|------|
+| [Context 管理](./context-management.md) | Goals、压缩、Plan 注入策略 |
+| [工具系统](../agent/docs/tools.md) | 工具定义、注册、双层记忆 |
+| [Skills 指南](../agent/docs/skills.md) | Skill 分类、编写、加载 |
+| [多模态支持](../agent/docs/multimodal.md) | 图片、PDF 处理 |
+| [知识管理](./knowledge.md) | 知识结构、检索、提取机制 |
+| [Scope 设计](./scope-design.md) | 知识可见性和权限控制 |
+| [Agent 设计决策](../agent/docs/decisions.md) | Agent Core 架构决策记录 |
+| [Gateway 设计决策](../gateway/docs/decisions.md) | Gateway 架构决策记录 |
+| [组织级概览](../gateway/docs/enterprise/overview.md) | 组织级 Agent 系统架构和规划 |
+| [Enterprise 实现](../gateway/docs/enterprise/implementation.md) | 认证、审计、多租户技术实现 |
+| [测试指南](./testing.md) | 测试策略和命令 |
+| [A2A 协议调研](./research/a2a-protocols.md) | 行业 A2A 通信协议和框架对比 |
+| [A2A 跨设备通信](./research/a2a-cross-device.md) | 跨设备 Agent 通信方案(内部) |
+| [A2A Trace 存储](./research/a2a-trace-storage.md) | 跨设备 Trace 存储方案详细设计 |
+| [MAMP 协议](./research/a2a-mamp-protocol.md) | 与外部 Agent 系统的通用交互协议 |
+| [A2A IM 系统](./a2a-im.md) | Agent 即时通讯系统架构和实现 |
+| [Gateway 架构](../gateway/docs/architecture.md) | Gateway 三层架构和设计决策 |
+| [Gateway 部署](../gateway/docs/deployment.md) | Gateway 部署模式和配置 |
+| [Gateway API](../gateway/docs/api.md) | Gateway API 完整参考 |

+ 169 - 0
docs/decisions.md → agent/docs/decisions.md

@@ -1156,4 +1156,173 @@ Rewind 事件 payload 中增加 `head_sequence` 字段,便于前端感知分
 
 
 **实现**:`agent/trace/run_api.py`, `agent/core/runner.py`, `agent/trace/api.py`
 **实现**:`agent/trace/run_api.py`, `agent/core/runner.py`, `agent/trace/api.py`
 
 
+---
+
+## Decision 24: 侧分支多轮 Agent 模式
+
+**日期**: 2026-03-09
+
+### 问题
+
+原有的压缩和反思使用单轮 LLM 调用,但这些任务可能需要多轮推理和工具调用才能做好:
+- **压缩**:可能需要查询 goal_tree 状态、分步总结
+- **反思**:可能需要先分析失败原因、再提取经验,或检查知识库避免重复
+
+单轮调用限制了 LLM 的推理能力,且改变 system prompt 或工具清单会导致缓存失效。
+
+### 决策
+
+**选择:侧分支在同一 agent loop 中以状态机模式运行**
+
+#### 24a. 核心设计
+
+侧分支不是递归调用 `_agent_loop`,而是在同一个循环中通过状态切换实现:
+
+```python
+# 主循环维护侧分支状态
+side_branch_ctx: Optional[SideBranchContext] = None
+
+for iteration in range(max_iterations):
+    # 进入侧分支:追加 prompt,设置状态
+    if needs_compression and not side_branch_ctx:
+        side_branch_ctx = SideBranchContext(...)
+        history.append({"role": "user", "content": compress_prompt})
+        continue
+
+    # 侧分支中:正常执行 LLM 调用和工具执行
+    result = await self.llm_call(history, tools=..., model=...)
+
+    # 退出侧分支:提取结果,回到起点
+    if side_branch_ctx and not tool_calls:
+        # 从数据库查询侧分支消息并提取 summary
+        all_messages = await trace_store.get_trace_messages(trace_id)
+        side_messages = [m for m in all_messages if m.branch_id == side_branch_ctx.branch_id]
+        summary = extract_summary(side_messages)
+
+        history = history[:side_branch_ctx.start_history_length]
+        # 创建主路径 summary 消息
+        side_branch_ctx = None
+        continue
+```
+
+**优势**:
+1. **缓存友好**:复用主路径的所有缓存,只有追加的 prompt 是新内容
+2. **工具自然可用**:不需要单独配置工具清单,agent 自由选择需要的工具
+3. **实现简洁**:不需要递归调用,状态管理清晰
+
+#### 24b. 侧分支上下文结构
+
+```python
+@dataclass
+class SideBranchContext:
+    type: str  # "compression" | "reflection"
+    branch_id: str
+    start_head_seq: int  # 起点的 head_seq
+    start_sequence: int  # 起点的 sequence
+    start_history_length: int  # 起点的 history 长度
+    start_iteration: int  # 侧分支开始时的 iteration
+    max_turns: int = 5  # 最大轮次
+```
+
+**设计说明**:
+1. **不维护 `side_messages` 列表**:所有侧分支消息已持久化到数据库(标记 `branch_id`),需要时通过查询获取,避免内存中的重复维护
+2. **复用主循环的 `iteration`**:不单独维护 `current_turn`,而是通过 `iteration - start_iteration` 计算侧分支已执行的轮次,简化计数逻辑
+
+#### 24c. 消息标记
+
+侧分支产生的消息通过 `branch_type` 和 `branch_id` 字段标记:
+- `branch_type`: "compression" | "reflection" | None(主路径)
+- `branch_id`: 同一侧分支的消息共享 branch_id
+- `parent_sequence`: 侧分支消息的 parent 指向主路径或前一条侧分支消息
+
+**关键设计**:`trace.head_sequence` 始终指向主路径的头节点。侧分支执行期间,`head_sequence` 保持在侧分支起点,不更新。侧分支完成后,创建主路径 summary 消息(parent 指向起点),然后更新 `head_sequence` 指向 summary。
+
+这样设计的好处:
+- `get_main_path_messages(trace_id, head_sequence)` 自然返回主路径消息
+- 侧分支消息通过 parent_sequence 链自动脱离主路径,无需额外过滤
+- 续跑时自动加载正确的主路径历史
+
+#### 24d. 停止条件
+
+侧分支使用与主 agent 相同的停止逻辑:
+- LLM 返回无工具调用 → 认为完成
+- 达到 `config.side_branch_max_turns` → 强制停止并处理:
+  - **压缩侧分支**:fallback 到单次 LLM 调用(无工具)
+  - **反思侧分支**:直接退出,不管结果
+
+用户在侧分支中追加的消息自动标记为侧分支消息,继续在侧分支中执行。
+
+#### 24e. 工具 context 传递
+
+侧分支信息通过 `context` 参数传递给工具,保持框架一致性:
+
+```python
+context = {
+    "store": self.trace_store,
+    "trace_id": trace_id,
+    "goal_id": current_goal_id,
+    "runner": self,
+    "goal_tree": goal_tree,
+    "knowledge_config": config.knowledge,
+    # 新增:侧分支信息
+    "side_branch": {
+        "type": side_branch_ctx.type,
+        "branch_id": side_branch_ctx.branch_id,
+        "is_side_branch": True,
+        "current_turn": side_branch_ctx.current_turn,
+        "max_turns": side_branch_ctx.max_turns,
+    } if side_branch_ctx else None,
+}
+```
+
+工具可以通过 `context.get("side_branch")` 感知自己是否在侧分支中执行,但当前不需要特殊处理。
+
+#### 24f. 主循环重构
+
+为避免主循环过于复杂,提取以下函数:
+- `_manage_context_usage()`: Context 用量检查、预警、压缩(整合 Level 1/2)
+- `_check_enter_side_branch()`: 检查是否需要进入侧分支
+- `_check_exit_side_branch()`: 检查是否需要退出侧分支
+- `_exit_side_branch()`: 执行退出逻辑(回到起点)
+- `_single_turn_compress()`: 单次 LLM 压缩(fallback 方案)
+
+主循环通过 `if not side_branch_ctx` 控制哪些逻辑只在主路径执行。
+
+#### 24g. 侧分支状态持久化
+
+侧分支状态存储在 `trace.context["active_side_branch"]`:
+- 进入侧分支时创建,记录 `max_turns`(来自 `RunConfig.side_branch_max_turns`,默认 5)
+- 每轮结束时更新 `current_turn`
+- 退出侧分支时清除
+- 续跑时自动恢复,使用持久化的 `max_turns` 值
+
+这确保了中断后可以继续完成侧分支,不浪费已执行的 LLM 调用。
+
+#### 24h. RunConfig 配置
+
+新增字段:
+- `side_branch_max_turns: int = 5` — 侧分支最大轮次,超过后强制退出
+- `force_side_branch: Optional[Literal["compression", "reflection"]] = None` — 强制进入侧分支(用于 API 手动触发压缩/反思)
+
+**force_side_branch 说明**:
+- 用于 API 接口手动触发压缩或反思(如 `/api/traces/{id}/compact`、`/api/traces/{id}/reflect`)
+- 设置后,agent loop 会在第一轮就进入指定类型的侧分支,而不是等待 context 超限
+- 侧分支完成后自动清除此配置(`config.force_side_branch = None`),避免影响后续续跑
+
+**API 触发实现**:
+- `/api/traces/{id}/reflect` — 设置 `RunConfig(force_side_branch="reflection")`,启动后台任务
+- `/api/traces/{id}/compact` — 设置 `RunConfig(force_side_branch="compression")`,启动后台任务
+- `agent/cli/interactive.py:manual_compact()` — 同样使用 `force_side_branch="compression"`,消费 `run()` 生成器
+
+**实现位置**:`agent/trace/run_api.py:reflect_trace`, `agent/trace/run_api.py:compact_trace`, `agent/cli/interactive.py:manual_compact`
+
+### 变更范围
+
+- `agent/trace/models.py` — Message 增加 `branch_type` 和 `branch_id` 字段
+- `agent/core/runner.py` — 增加 `SideBranchContext`,重构 `_agent_loop`
+- `agent/trace/compaction.py` — `_compress_history` 改为状态机模式
+- `agent/trace/protocols.py` — 查询接口支持过滤侧分支消息
+
+**实现**:`agent/core/runner.py:_agent_loop`, `agent/trace/models.py:Message`, `agent/trace/compaction.py`
+
 ---
 ---

+ 201 - 0
agent/docs/knowledge.md

@@ -0,0 +1,201 @@
+- 产生知识时区分:人、场景(例如飞书群,可以对应到tags)、业务(还有子业务)
+
+**知识结构**(单条知识)
+    id: 知识唯一标识
+    task: 任务描述,什么场景、在做什么
+
+    type: 知识类型(单选)
+        user_profile:用户画像(偏好、习惯、背景、约束)
+        strategy:具体执行经验,从执行过程中反思获得
+        tool:工具名称,简介,使用方法,优缺点对比,代码示例
+        usecase:用户背景,采用方案,实现步骤,问题,效果
+        definition:概念定义,技术原理,应用场景
+        plan:流程步骤,决策点,某个问题的总结方法论
+
+    tags: 业务标签(可有多个)
+        category: 子类别(如 preference, background, habit, constraint)
+        domain: 领域(如 coding_style, architecture)
+        其他自定义标签
+        scenario
+
+    scopes: 可见范围(可有多个,格式:{entity_type}:{entity_id})
+        user:{user_id}:用户级(个人可见)
+        agent:{agent_id}:Agent 级(特定 Agent 可见)
+        project:{project_id}:项目级(项目组可见)
+        team:{team_id}:团队级(部门可见)
+        org:{org_id}:组织级(全公司可见)
+        public:公开(所有人可见)
+
+    owner: 所有者(格式:{entity_type}:{entity_id},唯一)
+        谁创建的,谁有权修改/删除
+
+    content:
+        基于类型的具体内容,相对完整的一条知识
+
+    source:
+        name: 工具/资源名称(若有)
+        category:paper/exp/skill/book...
+        urls:知识来源的网站
+        agent_id:调用知识查询的agent名称
+        submitted_by:创建本条目的agent负责人名称
+        timestamp: 知识生成时的时间戳
+        trace_id: 来源 Trace ID(若有)
+
+    eval:基于使用反馈
+        helpful: 好用的次数
+        harmful:不好用的次数
+        confidence: 置信度(0-1)
+        helpful_history: [(query+trace_id+outcome), ]用于记录反馈时的调用总结
+        harmful_history: []
+
+知识检索机制
+    检索流程
+        1. 构建可见范围
+            根据执行上下文(user_id, agent_id, project_id, team_id, org_id)
+            构建用户的所有可见 scopes:
+                - user:{user_id}
+                - agent:{agent_id}
+                - project:{project_id}
+                - team:{team_id}
+                - org:{org_id}
+                - public
+
+        2. 向量检索 + 过滤
+            查询条件:
+                - 语义匹配(向量检索)
+                - scopes 过滤(知识的 scopes 与用户的 visible_scopes 有交集)
+                - type 过滤(可选,按知识类型过滤)
+
+        3. 按优先级排序
+            优先级:user > project > agent > team > org > public
+            取知识的 scopes 中优先级最高的进行排序
+
+    触发时机
+        - Agent 启动时:自动检索相关知识
+        - Goal focus 时:检索与当前目标相关的知识
+        - 主动调用:通过 get_knowledge 工具主动查询
+
+    实现位置
+        - agent/tools/builtin/knowledge.py: 知识管理工具(KnowHub API 封装)+ KnowledgeConfig
+        - agent/tools/builtin/knowledge.py: knowledge_search / knowledge_save 工具
+知识提取机制
+    触发时机
+        - 主动表达:用户明确表达偏好、纠正、提供背景信息
+        - 任务完成:任务完成后的反思总结
+        - 压缩消息:在压缩消息节点提取经验
+        - 用户反馈:用户对结果的 helpful/harmful 评价
+
+    提取流程
+        1. 识别知识类型
+            根据内容判断是 user_profile / strategy / tool / definition / plan / usecase
+
+        2. 结构化提取
+            - 提取核心内容
+            - 生成标签(category, domain 等)
+            - 确定 scopes(基于执行上下文)
+            - 设置 owner 和 visibility
+
+        3. 持久化存储
+            - 存入知识库(向量数据库 + 元数据)
+            - 记录来源(trace_id, agent_id, timestamp)
+            - 初始化评价(helpful=1, confidence=0.5)
+
+    实现位置(规划)
+        - agent/trace/compaction.py: 压缩时提取经验
+        - agent/tools/builtin/knowledge.py: knowledge_save 工具
+用户画像特殊处理
+    用户画像是一种特殊的知识类型(type=user_profile),具有以下特点:
+
+    获取方式
+        - 持续性:在整个对话过程中持续积累
+        - 多触发点:主动表达 > 任务完成 > 周期性总结
+        - 渐进式:逐步完善,置信度逐渐提高
+
+    存储特点
+        - type: user_profile
+        - scopes: 通常包含 user:{user_id},可能包含 project:{project_id}
+        - owner: user:{user_id}
+        - tags.category: preference | background | habit | constraint
+
+    检索优先级
+        - 在 Agent 启动时自动加载
+        - 优先级高于其他类型的知识
+        - 按 confidence 和 helpful 评分排序
+
+    示例
+        {
+            "id": "profile_001",
+            "type": "user_profile",
+            "task": "用户编码风格偏好",
+            "tags": {
+                "category": "preference",
+                "domain": "coding_style"
+            },
+            "scopes": ["user:123", "project:456"],
+            "owner": "user:123",
+            "visibility": "shared",
+            "content": "用户偏好使用 TypeScript 而非 JavaScript,注重类型安全",
+            "source": {
+                "agent_id": "general_assistant",
+                "trace_id": "trace-xxx",
+                "timestamp": "2026-03-03"
+            },
+            "eval": {
+                "helpful": 5,
+                "harmful": 0,
+                "confidence": 0.9
+            }
+        }
+存储结构(规划)
+    数据库表结构
+        CREATE TABLE knowledge (
+            id TEXT PRIMARY KEY,
+            type TEXT NOT NULL,              -- user_profile | strategy | tool | ...
+            task TEXT,
+            tags JSON,
+            scopes JSON,                     -- ["user:123", "project:456", ...]
+            owner TEXT NOT NULL,             -- "user:123" | "agent:xxx"
+            visibility TEXT,                 -- private | shared | org | public
+            content TEXT,
+            source JSON,
+            eval JSON,
+            embedding VECTOR(1536),          -- 向量检索
+            created_at TIMESTAMP,
+            updated_at TIMESTAMP
+        );
+
+        -- 索引
+        CREATE INDEX idx_type ON knowledge(type);
+        CREATE INDEX idx_owner ON knowledge(owner);
+        CREATE INDEX idx_visibility ON knowledge(visibility);
+        CREATE INDEX idx_scopes ON knowledge USING GIN(scopes);
+
+    文件系统结构
+        /workspace/knowledge/
+        ├── global/                   # 全局知识
+        │   ├── tools.json
+        │   ├── definitions.json
+        │   └── plans.json
+        │
+        ├── agents/                   # Agent 经验
+        │   ├── general_assistant/
+        │   ├── crawler_ops/
+        │   └── ...
+        │
+        └── users/                    # 用户画像
+            ├── user_123/
+            │   ├── profile.json      # 基础信息
+            │   ├── preferences.json  # 偏好
+            │   ├── habits.json       # 习惯
+            │   └── constraints.json  # 约束
+            └── ...
+
+TODO
+    1. ~~实现知识存储接口~~ → 已通过 KnowHub API 实现(agent/tools/builtin/knowledge.py)
+    2. ~~实现知识检索逻辑~~ → knowledge_search 工具
+    3. ~~实现 knowledge_save 工具~~ → 已实现
+    4. ~~实现 knowledge_search 工具~~ → 已实现
+    5. ~~在 Agent 启动时集成知识检索~~ → 已实现(goal focus 时注入)
+    6. ~~在 Goal focus 时集成知识检索~~ → 已实现(agent/trace/goal_tool.py:inject_knowledge_for_goal)
+    7. ~~在压缩消息时集成知识提取~~ → 已实现(agent/core/runner.py:_run_reflect)
+    8. 实现用户画像的特殊处理逻辑

+ 0 - 0
docs/multimodal.md → agent/docs/multimodal.md


+ 329 - 0
agent/docs/scope-design.md

@@ -0,0 +1,329 @@
+# Scope 设计文档
+
+## 概述
+
+Scope 系统用于控制知识的可见性和访问权限。采用**灵活的标签系统**,而非固定的层级结构。
+
+## 核心原则
+
+1. **知识类型与可见性分离**
+   - `type` 字段表示知识类型(What)
+   - `scopes` 字段表示可见范围(Who)
+
+2. **多 Scope 支持**
+   - 一条知识可以有多个 scope 标签
+   - 支持灵活的共享关系
+
+3. **明确的所有权**
+   - `owner` 字段表示唯一所有者
+   - 只有所有者有权修改/删除
+
+## Scope 格式
+
+```
+格式:{entity_type}:{entity_id}
+
+示例:
+- user:123
+- agent:general_assistant
+- project:456
+- team:frontend
+- org:company
+- public
+```
+
+## Scope 类型
+
+| Scope 类型 | 格式 | 说明 | 示例 |
+|-----------|------|------|------|
+| 用户级 | `user:{user_id}` | 用户个人可见 | `user:123` |
+| Agent 级 | `agent:{agent_id}` | 特定 Agent 可见 | `agent:crawler_ops` |
+| 项目级 | `project:{project_id}` | 项目组可见 | `project:456` |
+| 团队级 | `team:{team_id}` | 部门可见 | `team:frontend` |
+| 组织级 | `org:{org_id}` | 全公司可见 | `org:company` |
+| 公开 | `public` | 所有人可见 | `public` |
+
+## 数据结构
+
+```json
+{
+  "id": "knowledge_001",
+
+  "type": "user_profile",
+
+  "scopes": [
+    "user:123",
+    "project:456"
+  ],
+
+  "owner": "user:123",
+
+  "visibility": "shared",
+
+  "content": "...",
+  "tags": {...},
+  "source": {...},
+  "eval": {...}
+}
+```
+
+## 字段说明
+
+### type(知识类型)
+
+- `user_profile`:用户画像
+- `strategy`:执行经验
+- `tool`:工具知识
+- `usecase`:用例
+- `definition`:概念定义
+- `plan`:方法论
+
+### scopes(可见范围)
+
+- 数组类型,可包含多个 scope
+- 知识对所有 scopes 中的实体可见
+- 检索时匹配:知识的 scopes 与用户的 visible_scopes 有交集
+
+### owner(所有者)
+
+- 字符串类型,格式同 scope
+- 唯一所有者,有权修改/删除
+- 通常是创建者
+
+### visibility(可见性级别)
+
+快速过滤标签,用于 UI 展示和简单查询:
+
+- `private`:私有(仅所有者)
+- `shared`:共享(多个实体)
+- `org`:组织级
+- `public`:公开
+
+## 使用场景
+
+### 场景 1:用户的私有偏好
+
+```json
+{
+  "type": "user_profile",
+  "scopes": ["user:123"],
+  "owner": "user:123",
+  "visibility": "private",
+  "content": "用户偏好使用 TypeScript"
+}
+```
+
+### 场景 2:项目组共享的经验
+
+```json
+{
+  "type": "strategy",
+  "scopes": ["project:456"],
+  "owner": "agent:crawler_ops",
+  "visibility": "shared",
+  "content": "爬虫反爬策略:使用代理池 + 随机 UA"
+}
+```
+
+### 场景 3:跨项目的用户偏好
+
+```json
+{
+  "type": "user_profile",
+  "scopes": ["user:123", "project:456", "project:789"],
+  "owner": "user:123",
+  "visibility": "shared",
+  "content": "用户在多个项目中都偏好使用 React"
+}
+```
+
+### 场景 4:Agent 间共享的工具知识
+
+```json
+{
+  "type": "tool",
+  "scopes": [
+    "agent:crawler_ops",
+    "agent:content_library",
+    "agent:general_assistant"
+  ],
+  "owner": "agent:crawler_ops",
+  "visibility": "shared",
+  "content": "Selenium 使用技巧:headless 模式配置"
+}
+```
+
+### 场景 5:组织级公开知识
+
+```json
+{
+  "type": "definition",
+  "scopes": ["org:company"],
+  "owner": "org:company",
+  "visibility": "org",
+  "content": "公司技术栈:React + TypeScript + Node.js"
+}
+```
+
+### 场景 6:完全公开的知识
+
+```json
+{
+  "type": "tool",
+  "scopes": ["public"],
+  "owner": "org:company",
+  "visibility": "public",
+  "content": "Git 常用命令速查表"
+}
+```
+
+## 检索逻辑
+
+### 构建可见范围
+
+```python
+def build_visible_scopes(context):
+    """
+    根据执行上下文构建用户的所有可见 scopes
+
+    context = {
+        "user_id": "123",
+        "agent_id": "general_assistant",
+        "project_id": "456",
+        "team_id": "frontend",
+        "org_id": "company"
+    }
+    """
+    scopes = []
+
+    if context.get("user_id"):
+        scopes.append(f"user:{context['user_id']}")
+
+    if context.get("agent_id"):
+        scopes.append(f"agent:{context['agent_id']}")
+
+    if context.get("project_id"):
+        scopes.append(f"project:{context['project_id']}")
+
+    if context.get("team_id"):
+        scopes.append(f"team:{context['team_id']}")
+
+    if context.get("org_id"):
+        scopes.append(f"org:{context['org_id']}")
+
+    scopes.append("public")
+
+    return scopes
+```
+
+### 检索查询
+
+```python
+def search_knowledge(query, context, knowledge_type=None):
+    """
+    检索知识
+
+    query: 查询内容
+    context: 用户上下文
+    knowledge_type: 可选,过滤知识类型
+    """
+    # 1. 构建可见范围
+    visible_scopes = build_visible_scopes(context)
+
+    # 2. 构建查询条件
+    filters = {
+        "scopes": {"$in": visible_scopes}  # 交集匹配
+    }
+
+    if knowledge_type:
+        filters["type"] = knowledge_type
+
+    # 3. 向量检索 + 过滤
+    results = vector_db.search(
+        query=query,
+        filter=filters,
+        top_k=10
+    )
+
+    # 4. 按优先级排序
+    return rank_by_scope_priority(results, context)
+```
+
+### 优先级排序
+
+```python
+def rank_by_scope_priority(results, context):
+    """
+    按 scope 优先级排序
+
+    优先级:user > project > agent > team > org > public
+    """
+    priority_map = {
+        f"user:{context.get('user_id')}": 6,
+        f"project:{context.get('project_id')}": 5,
+        f"agent:{context.get('agent_id')}": 4,
+        f"team:{context.get('team_id')}": 3,
+        f"org:{context.get('org_id')}": 2,
+        "public": 1
+    }
+
+    def get_priority(knowledge):
+        # 取知识的 scopes 中优先级最高的
+        max_priority = 0
+        for scope in knowledge["scopes"]:
+            max_priority = max(max_priority, priority_map.get(scope, 0))
+        return max_priority
+
+    return sorted(results, key=get_priority, reverse=True)
+```
+
+## 权限控制
+
+### 读权限
+
+用户可以读取知识,当且仅当:
+- 知识的 `scopes` 与用户的 `visible_scopes` 有交集
+
+### 写权限
+
+用户可以修改/删除知识,当且仅当:
+- 用户是知识的 `owner`
+
+### 特殊情况
+
+- 管理员可以修改/删除所有知识
+- 组织级知识(owner=org:xxx)可以由管理员管理
+
+## 实现位置
+
+- `agent/tools/builtin/knowledge.py`:知识管理工具(KnowHub API 封装)+ KnowledgeConfig
+
+## 扩展性
+
+### 新增 Scope 类型
+
+如需新增 scope 类型(如 `department:{dept_id}`),只需:
+
+1. 在 `build_visible_scopes` 中添加逻辑
+2. 在 `rank_by_scope_priority` 中添加优先级
+3. 无需修改数据结构和存储逻辑
+
+### 升级为 ACL
+
+如需更细粒度的权限控制,可扩展为 ACL 系统:
+
+```json
+{
+  "id": "knowledge_001",
+  "type": "strategy",
+  "owner": "user:123",
+  "acl": [
+    {"entity": "user:123", "permission": "read_write"},
+    {"entity": "project:456", "permission": "read"},
+    {"entity": "agent:general_assistant", "permission": "read"}
+  ]
+}
+```
+
+但对于大多数场景,当前的 scope 标签系统已经足够。
+

+ 2 - 2
docs/skills.md → agent/docs/skills.md

@@ -192,7 +192,7 @@ skill(skill_name="browser-use")
 ```
 ```
 
 
 **实现位置**:
 **实现位置**:
-- `agent/memory/skill_loader.py:load_skills_from_dir()` - 自动加载机制
+- `agent/skill/skill_loader.py:load_skills_from_dir()` - 自动加载机制
 - `agent/tools/builtin/skill.py` - skill 工具(动态加载)
 - `agent/tools/builtin/skill.py` - skill 工具(动态加载)
 
 
 详见 [`SKILLS_SYSTEM.md`](../SKILLS_SYSTEM.md)
 详见 [`SKILLS_SYSTEM.md`](../SKILLS_SYSTEM.md)
@@ -231,4 +231,4 @@ SKILLS_DIR=./skills
 - 示例:`examples/feature_extract/run.py`
 - 示例:`examples/feature_extract/run.py`
 - Skill 文件:`agent/skills/` 目录
 - Skill 文件:`agent/skills/` 目录
 - 工具实现:`agent/tools/builtin/skill.py`
 - 工具实现:`agent/tools/builtin/skill.py`
-- 加载器实现:`agent/memory/skill_loader.py`
+- 加载器实现:`agent/skill/skill_loader.py`

+ 136 - 35
docs/tools.md → agent/docs/tools.md

@@ -25,7 +25,7 @@
 from reson_agent import tool, ToolResult, ToolContext
 from reson_agent import tool, ToolResult, ToolContext
 
 
 @tool()
 @tool()
-async def my_tool(arg: str, ctx: ToolContext) -> ToolResult:
+async def my_tool(arg: str, context: Optional[ToolContext] = None) -> ToolResult:
     return ToolResult(
     return ToolResult(
         title="Success",
         title="Success",
         output="Result content"
         output="Result content"
@@ -44,16 +44,70 @@ async def my_tool(arg: str, ctx: ToolContext) -> ToolResult:
 1. 定义工具
 1. 定义工具
    ↓ @tool() 装饰器
    ↓ @tool() 装饰器
 2. 自动注册到 ToolRegistry
 2. 自动注册到 ToolRegistry
-   ↓ 生成 OpenAI Tool Schema
+   ↓ 生成 OpenAI Tool Schema(跳过 hidden_params)
 3. LLM 选择工具并生成参数
 3. LLM 选择工具并生成参数
    ↓ registry.execute(name, args)
    ↓ registry.execute(name, args)
-4. 注入 uid 和 context
+4. 注入框架参数(hidden_params + inject_params)
    ↓ 调用工具函数
    ↓ 调用工具函数
 5. 返回 ToolResult
 5. 返回 ToolResult
    ↓ 转换为 LLM 消息
    ↓ 转换为 LLM 消息
 6. 添加到对话历史
 6. 添加到对话历史
 ```
 ```
 
 
+### 参数注入机制
+
+工具参数分为三类:
+
+1. **业务参数**:LLM 可见,由 LLM 填写(如 `query`, `limit`)
+2. **隐藏参数**:LLM 不可见,框架自动注入(如 `context`, `uid`)
+3. **注入参数**:LLM 可见,框架自动注入默认值或与 LLM 值合并(如 `owner`, `tags`)
+
+```python
+@tool(
+    hidden_params=["context", "owner"],  # 不生成 schema,LLM 看不到
+    inject_params={                       # 声明注入规则
+        "owner": {"mode": "default", "key": "knowledge_config.owner"},
+        "tags":  {"mode": "merge",   "key": "knowledge_config.default_tags"},
+        "scopes": {"mode": "merge",  "key": "knowledge_config.default_scopes"},
+    }
+)
+async def knowledge_save(
+    task: str,                          # 业务参数:LLM 填写
+    content: str,                       # 业务参数:LLM 填写
+    types: List[str],                   # 业务参数:LLM 填写
+    tags: Optional[Dict] = None,        # 注入参数:LLM 可填,框架合并默认值
+    scopes: Optional[List] = None,      # 注入参数:LLM 可填,框架合并默认值
+    owner: Optional[str] = None,        # 隐藏参数:LLM 看不到,框架注入
+    context: Optional[ToolContext] = None,  # 隐藏参数:LLM 看不到
+) -> ToolResult:
+    """保存知识到知识库"""
+    ...
+```
+
+**inject_params 声明格式**:
+
+```python
+inject_params={
+    "param_name": {
+        "mode": "default" | "merge",  # 注入模式
+        "key": "config_obj.field",    # 从 context 中取值的路径
+    }
+}
+```
+
+- `mode: "default"`:LLM 未提供时注入框架值
+- `mode: "merge"`:框架值与 LLM 值合并。dict 按 key 合并(框架 key 不可被覆盖,LLM 可追加新 key);list 合并去重
+
+**值的来源**:通过 `key` 指定从 `context` 中取值的路径(如 `"knowledge_config.default_tags"` 表示 `context["knowledge_config"].default_tags`)。runner 在调用 `execute()` 时将配置对象放入 context,框架根据 key 路径自动取值。
+
+**注入时机**:
+- Schema 生成时:跳过 `hidden_params`,不暴露给 LLM
+- 工具执行前:注入 `hidden_params` 和 `inject_params`
+
+**实现位置**:
+- Schema 生成:`agent/tools/schema.py:SchemaGenerator.generate()`
+- 参数注入:`agent/tools/registry.py:ToolRegistry.execute()`
+
 ---
 ---
 
 
 ## 定义工具
 ## 定义工具
@@ -64,23 +118,24 @@ async def my_tool(arg: str, ctx: ToolContext) -> ToolResult:
 from reson_agent import tool
 from reson_agent import tool
 
 
 @tool()
 @tool()
-async def hello(name: str, uid: str = "") -> str:
+async def hello(name: str) -> str:
     """向用户问好"""
     """向用户问好"""
     return f"Hello, {name}!"
     return f"Hello, {name}!"
 ```
 ```
 
 
 **要点**:
 **要点**:
-- `uid` 参数由框架自动注入(用户不传递)
 - 可以是同步或异步函数
 - 可以是同步或异步函数
 - 返回值自动序列化为 JSON
 - 返回值自动序列化为 JSON
+- 所有参数默认对 LLM 可见
 
 
-### 带完整注释
+### 带框架参数
 
 
 ```python
 ```python
-@tool()
+@tool(hidden_params=["context", "uid"])
 async def search_notes(
 async def search_notes(
     query: str,
     query: str,
     limit: int = 10,
     limit: int = 10,
+    context: Optional[ToolContext] = None,
     uid: str = ""
     uid: str = ""
 ) -> str:
 ) -> str:
     """
     """
@@ -89,14 +144,50 @@ async def search_notes(
     Args:
     Args:
         query: 搜索关键词
         query: 搜索关键词
         limit: 返回结果数量
         limit: 返回结果数量
+    """
+    # context 和 uid 由框架注入,LLM 看不到这两个参数
+    ...
+```
 
 
-    Returns:
-        JSON 格式的搜索结果
+### 带参数注入
+
+```python
+@tool(
+    hidden_params=["context", "owner"],
+    inject_params={
+        "owner": {"mode": "default", "key": "knowledge_config.owner"},
+        "tags":  {"mode": "merge",   "key": "knowledge_config.default_tags"},
+        "scopes": {"mode": "merge",  "key": "knowledge_config.default_scopes"},
+    }
+)
+async def knowledge_save(
+    task: str,
+    content: str,
+    types: List[str],
+    tags: Optional[Dict] = None,  # LLM 可填,框架合并默认值
+    scopes: Optional[List] = None,  # LLM 可填,框架合并默认值
+    owner: Optional[str] = None,  # LLM 看不到,框架注入
+    context: Optional[ToolContext] = None
+) -> ToolResult:
+    """
+    保存知识
+
+    Args:
+        task: 任务描述
+        content: 知识内容
+        types: 知识类型
+        tags: 业务标签(可选,框架合并默认值)
+        scopes: 可见范围(可选,框架合并默认值)
     """
     """
-    # 支持自动从 docstring 提取 function description 和 parameter descriptions
     ...
     ...
 ```
 ```
 
 
+**注入规则**:
+- `inject_params` 的 value 是一个 dict,包含:
+  - `mode`: `"default"`(LLM 未提供则注入)或 `"merge"`(与 LLM 值合并)
+  - `key`: 从 context 中取值的路径(如 `"knowledge_config.default_tags"`)
+- 参数同时在 `hidden_params` 中时,LLM 不可见,框架直接注入
+
 ### 带 UI 元数据
 ### 带 UI 元数据
 
 
 ```python
 ```python
@@ -228,16 +319,16 @@ async def generate_report() -> ToolResult:
 
 
 ### 基本概念
 ### 基本概念
 
 
-工具函数可以声明需要 `ToolContext` 参数,框架自动注入。
+工具函数可以声明需要 `ToolContext` 参数,框架自动注入。需要在 `@tool()` 装饰器中声明 `hidden_params=["context"]`,使其对 LLM 不可见。
 
 
 ```python
 ```python
 from reson_agent import ToolContext
 from reson_agent import ToolContext
 
 
-@tool()
-async def get_current_state(ctx: ToolContext) -> ToolResult:
+@tool(hidden_params=["context"])
+async def get_current_state(context: Optional[ToolContext] = None) -> ToolResult:
     return ToolResult(
     return ToolResult(
         title="Current state",
         title="Current state",
-        output=f"Trace ID: {ctx.trace_id}\nStep ID: {ctx.step_id}"
+        output=f"Trace ID: {context.trace_id}\nStep ID: {context.step_id}"
     )
     )
 ```
 ```
 
 
@@ -250,56 +341,66 @@ class ToolContext(Protocol):
     step_id: str                # 当前 Step ID
     step_id: str                # 当前 Step ID
     uid: Optional[str]          # 用户 ID
     uid: Optional[str]          # 用户 ID
 
 
+    # 扩展字段(由 runner 注入)
+    store: Optional[TraceStore]     # Trace 存储
+    runner: Optional[AgentRunner]   # Runner 实例
+    goal_tree: Optional[GoalTree]   # 目标树
+    goal_id: Optional[str]          # 当前 Goal ID
+    config: Optional[RunConfig]     # 运行配置
+
     # 浏览器相关(Browser-Use 集成)
     # 浏览器相关(Browser-Use 集成)
     browser_session: Optional[Any]      # 浏览器会话
     browser_session: Optional[Any]      # 浏览器会话
     page_url: Optional[str]             # 当前页面 URL
     page_url: Optional[str]             # 当前页面 URL
     file_system: Optional[Any]          # 文件系统访问
     file_system: Optional[Any]          # 文件系统访问
     sensitive_data: Optional[Dict]      # 敏感数据
     sensitive_data: Optional[Dict]      # 敏感数据
 
 
-    # 扩展字段
-    context: Optional[Dict[str, Any]]   # 额外上下文
+    # 额外上下文
+    context: Optional[Dict[str, Any]]   # 额外上下文数据
 ```
 ```
 
 
 ### 使用示例
 ### 使用示例
 
 
 ```python
 ```python
-@tool()
-async def analyze_current_page(ctx: ToolContext) -> ToolResult:
+@tool(hidden_params=["context"])
+async def analyze_current_page(context: Optional[ToolContext] = None) -> ToolResult:
     """分析当前浏览器页面"""
     """分析当前浏览器页面"""
 
 
-    if not ctx.browser_session:
+    if not context or not context.browser_session:
         return ToolResult(
         return ToolResult(
             title="Error",
             title="Error",
             error="Browser session not available"
             error="Browser session not available"
         )
         )
 
 
     # 使用浏览器会话
     # 使用浏览器会话
-    page_content = await ctx.browser_session.get_content()
+    page_content = await context.browser_session.get_content()
 
 
     return ToolResult(
     return ToolResult(
-        title=f"Analyzed {ctx.page_url}",
+        title=f"Analyzed {context.page_url}",
         output=page_content,
         output=page_content,
-        long_term_memory=f"Analyzed page at {ctx.page_url}"
+        long_term_memory=f"Analyzed page at {context.page_url}"
     )
     )
 ```
 ```
 
 
 ### 创建 ToolContext
 ### 创建 ToolContext
 
 
-```python
-from reson_agent import ToolContextImpl
+Runner 在执行工具时自动创建并注入 context:
 
 
-ctx = ToolContextImpl(
-    trace_id="trace_123",
-    step_id="step_456",
-    uid="user_789",
-    page_url="https://example.com"
-)
+```python
+# 在 AgentRunner._agent_loop 中
+context = {
+    "store": self.trace_store,
+    "trace_id": trace_id,
+    "goal_id": current_goal_id,
+    "runner": self,
+    "goal_tree": goal_tree,
+    "config": config,
+}
 
 
-# 执行工具
-result = await registry.execute(
-    "analyze_current_page",
-    arguments={},
-    context=ctx
+result = await self.tools.execute(
+    tool_name,
+    tool_args,
+    uid=config.uid or "",
+    context=context
 )
 )
 ```
 ```
 
 

+ 1 - 1
docs/trace-api.md → agent/docs/trace-api.md

@@ -304,7 +304,7 @@ GET /api/experiences
 ### 连接
 ### 连接
 
 
 ```
 ```
-ws://localhost:8000/api/traces/{trace_id}/watch?since_event_id=0
+ws://43.106.118.91:8000/api/traces/{trace_id}/watch?since_event_id=0
 ```
 ```
 
 
 ### 事件类型
 ### 事件类型

+ 13 - 13
agent/llm/openrouter.py

@@ -486,7 +486,7 @@ async def _openrouter_anthropic_call(
 
 
     # Resolve model name for OpenRouter (e.g. "claude-sonnet-4.5" → "anthropic/claude-sonnet-4-5-20250929")
     # Resolve model name for OpenRouter (e.g. "claude-sonnet-4.5" → "anthropic/claude-sonnet-4-5-20250929")
     resolved_model = _resolve_openrouter_model(model)
     resolved_model = _resolve_openrouter_model(model)
-    logger.info("[OpenRouter/Anthropic] model: %s → %s", model, resolved_model)
+    logger.debug("[OpenRouter/Anthropic] model: %s → %s", model, resolved_model)
 
 
     # 跨 Provider 续跑时,重写不兼容的 tool_call_id 为 toolu_ 前缀
     # 跨 Provider 续跑时,重写不兼容的 tool_call_id 为 toolu_ 前缀
     messages = _normalize_tool_call_ids(messages, "toolu")
     messages = _normalize_tool_call_ids(messages, "toolu")
@@ -527,20 +527,20 @@ async def _openrouter_anthropic_call(
         logger.info(f"[OpenRouter/Anthropic] Extended thinking enabled (budget: {thinking_config['budget_tokens']} tokens)")
         logger.info(f"[OpenRouter/Anthropic] Extended thinking enabled (budget: {thinking_config['budget_tokens']} tokens)")
 
 
     # Debug: 检查 cache_control 是否存在
     # Debug: 检查 cache_control 是否存在
-    cache_control_count = 0
-    if isinstance(system_prompt, list):
-        for block in system_prompt:
-            if isinstance(block, dict) and "cache_control" in block:
-                cache_control_count += 1
-    for msg in anthropic_messages:
-        content = msg.get("content", "")
-        if isinstance(content, list):
-            for block in content:
+    if logger.isEnabledFor(logging.DEBUG):
+        cache_control_count = 0
+        if isinstance(system_prompt, list):
+            for block in system_prompt:
                 if isinstance(block, dict) and "cache_control" in block:
                 if isinstance(block, dict) and "cache_control" in block:
                     cache_control_count += 1
                     cache_control_count += 1
-    if cache_control_count > 0:
-        print(f"[OpenRouter/Anthropic] 发现 {cache_control_count} 个 cache_control 标记")
-        logger.info(f"[OpenRouter/Anthropic] 发现 {cache_control_count} 个 cache_control 标记")
+        for msg in anthropic_messages:
+            content = msg.get("content", "")
+            if isinstance(content, list):
+                for block in content:
+                    if isinstance(block, dict) and "cache_control" in block:
+                        cache_control_count += 1
+        if cache_control_count > 0:
+            logger.debug(f"[OpenRouter/Anthropic] 发现 {cache_control_count} 个 cache_control 标记")
 
 
     headers = {
     headers = {
         "Authorization": f"Bearer {api_key}",
         "Authorization": f"Bearer {api_key}",

+ 0 - 37
agent/memory/__init__.py

@@ -1,37 +0,0 @@
-"""
-Memory - 记忆系统
-
-核心职责:
-1. Experience 和 Skill 数据模型
-2. MemoryStore 和 StateStore 接口定义
-3. 内存存储实现(MemoryMemoryStore, MemoryStateStore)
-4. Skill 加载器(从 markdown 加载技能)
-"""
-
-# 数据模型
-from agent.memory.models import Experience, Skill
-
-# 存储接口
-from agent.memory.protocols import MemoryStore, StateStore
-
-# 内存存储实现
-from agent.memory.stores import MemoryMemoryStore, MemoryStateStore
-
-# Skill 加载器
-from agent.memory.skill_loader import SkillLoader, load_skills_from_dir
-
-
-__all__ = [
-    # 模型
-    "Experience",
-    "Skill",
-    # 存储接口
-    "MemoryStore",
-    "StateStore",
-    # 存储实现
-    "MemoryMemoryStore",
-    "MemoryStateStore",
-    # Skill 加载
-    "SkillLoader",
-    "load_skills_from_dir",
-]

+ 0 - 106
agent/memory/protocols.py

@@ -1,106 +0,0 @@
-"""
-Storage Protocols - 存储接口定义
-
-使用 Protocol 定义接口,允许不同的存储实现(内存、PostgreSQL、Neo4j 等)
-
-TraceStore 已移动到 agent.execution.protocols
-"""
-
-from typing import Protocol, List, Optional, Dict, Any, runtime_checkable
-
-from agent.memory.models import Experience, Skill
-
-
-@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:
-        """删除"""
-        ...

+ 0 - 10
agent/memory/skills/research.md

@@ -1,10 +0,0 @@
----
-name: research
-description: 信息调研,使用搜索工具和浏览器获取外部信息
----
-
-## 信息调研
-
-你可以通过联网搜索工具 `search_posts` 获取来自 Github、小红书、微信公众号、知乎等渠道的信息。对于需要深度交互的网页内容,使用浏览器工具进行操作。
-
-调研过程可能需要多次搜索,比如基于搜索结果中获得的启发或信息启动新的搜索,直到得到令人满意的答案。你可以使用 `goal` 工具管理搜索的过程,或者使用文档记录搜索的中间或最终结果。

+ 0 - 103
agent/memory/stores.py

@@ -1,103 +0,0 @@
-"""
-Memory Implementation - 内存存储实现
-
-用于测试和简单场景,数据不持久化
-"""
-
-from typing import Dict, List, Optional, Any
-from datetime import datetime
-
-from agent.memory.models import Experience, Skill
-
-
-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)

+ 16 - 0
agent/skill/__init__.py

@@ -0,0 +1,16 @@
+"""
+Skill - 技能系统
+
+核心职责:
+1. Skill 数据模型
+2. Skill 加载器(从 Markdown 加载技能)
+"""
+
+from agent.skill.models import Skill
+from agent.skill.skill_loader import SkillLoader, load_skills_from_dir
+
+__all__ = [
+    "Skill",
+    "SkillLoader",
+    "load_skills_from_dir",
+]

+ 4 - 82
agent/memory/models.py → agent/skill/models.py

@@ -1,93 +1,17 @@
 """
 """
-Memory 数据模型
-
-Experience: 经验规则(条件 + 规则 + 证据)
-Skill: 技能(从经验归纳的高层知识)
+Skill 数据模型
 """
 """
 
 
 from dataclasses import dataclass, field
 from dataclasses import dataclass, field
 from datetime import datetime
 from datetime import datetime
-from typing import Dict, Any, List, Optional, Literal
+from typing import Dict, Any, List, Optional
 import uuid
 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
 @dataclass
 class Skill:
 class Skill:
     """
     """
-    技能 - 从经验归纳的高层知识
+    技能 - 从 Markdown 文件加载的领域知识
 
 
     技能可以形成层次结构(通过 parent_id)
     技能可以形成层次结构(通过 parent_id)
     """
     """
@@ -104,7 +28,7 @@ class Skill:
     # 内容
     # 内容
     content: Optional[str] = None  # 完整的 skill 内容(Markdown)
     content: Optional[str] = None  # 完整的 skill 内容(Markdown)
     guidelines: List[str] = field(default_factory=list)
     guidelines: List[str] = field(default_factory=list)
-    derived_from: List[str] = field(default_factory=list)  # experience_ids
+    derived_from: List[str] = field(default_factory=list)
 
 
     # 版本
     # 版本
     version: int = 1
     version: int = 1
@@ -164,11 +88,9 @@ class Skill:
 
 
         优先使用完整的 content(如果有),否则使用 description + guidelines
         优先使用完整的 content(如果有),否则使用 description + guidelines
         """
         """
-        # 如果有完整的 content,直接使用
         if self.content:
         if self.content:
             return self.content.strip()
             return self.content.strip()
 
 
-        # 否则使用旧的格式(向后兼容)
         lines = [f"### {self.name}", self.description]
         lines = [f"### {self.name}", self.description]
         if self.guidelines:
         if self.guidelines:
             lines.append("指导原则:")
             lines.append("指导原则:")

+ 2 - 2
agent/memory/skill_loader.py → agent/skill/skill_loader.py

@@ -39,7 +39,7 @@ from pathlib import Path
 from typing import List, Dict, Optional
 from typing import List, Dict, Optional
 import logging
 import logging
 
 
-from agent.memory.models import Skill
+from agent.skill.models import Skill
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
@@ -383,7 +383,7 @@ def load_skills_from_dir(skills_dir: Optional[str] = None) -> List[Skill]:
     """
     """
     all_skills = []
     all_skills = []
 
 
-    # 1. 加载内置 skills(agent/memory/skills/)
+    # 1. 加载内置 skills(agent/skill/skills/)
     builtin_skills_dir = Path(__file__).parent / "skills"
     builtin_skills_dir = Path(__file__).parent / "skills"
     if builtin_skills_dir.exists():
     if builtin_skills_dir.exists():
         loader = SkillLoader(str(builtin_skills_dir))
         loader = SkillLoader(str(builtin_skills_dir))

+ 7 - 2
agent/memory/skills/browser.md → agent/skill/skills/browser.md

@@ -23,12 +23,17 @@ description: 浏览器自动化工具使用指南
 - **操作后等待**: 任何可能触发页面变化的操作(点击、输入、滚动)后都要调用 `browser_wait`
 - **操作后等待**: 任何可能触发页面变化的操作(点击、输入、滚动)后都要调用 `browser_wait`
 - **登录处理**:
 - **登录处理**:
   - **正常登录**:当遇到需要登录的网页时,使用 `browser_load_cookies` 来登录
   - **正常登录**:当遇到需要登录的网页时,使用 `browser_load_cookies` 来登录
-  - **首次登录**:当没有该网站的 cookie 时,点击进入登录界面,然后等待人类来登录,登录后使用 `browser_export_cookies` 将账户信息存储下来
+  - **首次登录**:当没有该网站的 cookie 时,需要请求人类协助登录:
+    1. 调用 `browser_get_live_url` 获取云浏览器实时画面链接
+    2. 导航到目标网站的登录页面
+    3. 通过 `feishu_send_message_to_contact` 将 live URL 发送给相关人员,请求其在浏览器中完成登录
+    4. 使用 `feishu_get_contact_replies(contact_name="...", wait_time_seconds=300)` 等待对方回复确认登录完成
+    5. 收到回复后使用 `browser_export_cookies` 将登录态保存下来
 - **复杂操作用JS**: 当标准工具无法满足时,使用 `browser_evaluate` 执行 JavaScript 代码
 - **复杂操作用JS**: 当标准工具无法满足时,使用 `browser_evaluate` 执行 JavaScript 代码
 
 
 ### 工具分类
 ### 工具分类
 
 
-**导航**: browser_navigate_to_url, browser_search_web, browser_go_back, browser_wait
+**导航**: browser_get_live_url, browser_navigate_to_url, browser_search_web, browser_go_back, browser_wait
 **交互**: browser_click_element, browser_input_text, browser_send_keys, browser_upload_file
 **交互**: browser_click_element, browser_input_text, browser_send_keys, browser_upload_file
 **视图**: browser_scroll_page, browser_find_text, browser_screenshot
 **视图**: browser_scroll_page, browser_find_text, browser_screenshot
 **提取**: browser_extract_content, browser_read_long_content, browser_get_page_html, browser_get_selector_map, browser_get_visual_selector_map
 **提取**: browser_extract_content, browser_read_long_content, browser_get_page_html, browser_get_selector_map, browser_get_visual_selector_map

+ 10 - 5
agent/memory/skills/core.md → agent/skill/skills/core.md

@@ -65,11 +65,11 @@ goal(abandon="方案A需要Redis,环境没有")
 4. **计划可调整**:根据执行情况随时追加、跳过或放弃目标
 4. **计划可调整**:根据执行情况随时追加、跳过或放弃目标
 5. **使用 ID 定位**:focus、after、under 参数使用目标的 ID(如 "1", "2.1")
 5. **使用 ID 定位**:focus、after、under 参数使用目标的 ID(如 "1", "2.1")
 
 
-### 经验复用
+### 知识复用
 
 
-在**启动新任务**、**拆分复杂目标**或**遇到执行障碍**时,应主动调用 `get_experience` 获取k条历史成功经验或避坑指南。
+在**启动新任务**、**拆分复杂目标**或**遇到执行障碍**时,应主动调用 `knowledge_search` 获取相关的历史经验或避坑指南。
 **使用示例:**
 **使用示例:**
-`get_experience(query="如何处理浏览器点击不生效的问题")`
+`knowledge_search(query="如何处理浏览器点击不生效的问题", types=["strategy", "tool"])`
 
 
 ## 信息调研
 ## 信息调研
 
 
@@ -97,12 +97,17 @@ goal(abandon="方案A需要Redis,环境没有")
 - **操作后等待**: 任何可能触发页面变化的操作(点击、输入、滚动)后都要调用 `browser_wait`
 - **操作后等待**: 任何可能触发页面变化的操作(点击、输入、滚动)后都要调用 `browser_wait`
 - **登录处理**:
 - **登录处理**:
   - **正常登录**:当遇到需要登录的网页时,使用`browser_load_cookies`来登录
   - **正常登录**:当遇到需要登录的网页时,使用`browser_load_cookies`来登录
-  - **首次登录**:当没有该网站的cookie时,点击进入登录界面,然后等待人类来登录,登录后使用`browser_export_cookies`将账户信息存储下来
+  - **首次登录**:当没有该网站的cookie时,需要请求人类协助登录:
+    1. 调用 `browser_get_live_url` 获取云浏览器实时画面链接
+    2. 导航到目标网站的登录页面
+    3. 通过 `feishu_send_message_to_contact` 将 live URL 发送给相关人员,请求其在浏览器中完成登录
+    4. 使用 `feishu_get_contact_replies(contact_name="...", wait_time_seconds=300)` 等待对方回复确认登录完成
+    5. 收到回复后使用 `browser_export_cookies` 将登录态保存下来
 - **复杂操作用JS**: 当标准工具无法满足时,使用 `browser_evaluate` 执行JavaScript代码
 - **复杂操作用JS**: 当标准工具无法满足时,使用 `browser_evaluate` 执行JavaScript代码
 
 
 ### 工具分类
 ### 工具分类
 
 
-**导航**: browser_navigate_to_url, browser_search_web, browser_go_back, browser_wait
+**导航**: browser_get_live_url, browser_navigate_to_url, browser_search_web, browser_go_back, browser_wait
 **交互**: browser_click_element, browser_input_text, browser_send_keys, browser_upload_file
 **交互**: browser_click_element, browser_input_text, browser_send_keys, browser_upload_file
 **视图**: browser_scroll_page, browser_find_text, browser_screenshot
 **视图**: browser_scroll_page, browser_find_text, browser_screenshot
 **提取**: browser_extract_content, browser_read_long_content, browser_get_page_html, browser_get_selector_map, browser_get_visual_selector_map
 **提取**: browser_extract_content, browser_read_long_content, browser_get_page_html, browser_get_selector_map, browser_get_visual_selector_map

+ 0 - 0
agent/memory/skills/planning.md → agent/skill/skills/planning.md


+ 419 - 0
agent/skill/skills/research.md

@@ -0,0 +1,419 @@
+---
+name: atomic_research
+description: 知识调研 - 根据目标和任务自动执行搜索,返回结构化知识列表
+---
+
+## 信息调研
+
+你可以通过联网搜索工具 `search_posts` 获取来自 Github、小红书、微信公众号、知乎等渠道的信息。对于需要深度交互的网页内容,使用浏览器工具进行操作。
+
+## 调研过程可能需要多次搜索,比如基于搜索结果中获得的启发或信息启动新的搜索,直到得到令人满意的答案。你可以使用 `goal` 工具管理搜索的过程,或者使用文档记录搜索的中间或最终结果。(可以着重参考browser的工具来辅助搜索)
+
+## 工作流程
+
+### 输入
+
+- **目标**:要达成的目标(如"找到 PDF 表格提取的最佳方案")
+- **任务**:具体的任务描述(如"从复杂 PDF 中提取表格数据")
+
+### 执行流程
+
+**Step 1: 拆解搜索维度**
+
+```
+goal(add="搜索工具, 搜索案例, 搜索方法论")
+```
+
+**Step 2: 多维度搜索**
+
+- 搜索工具:`search_posts(query="PDF table extraction tool")`
+- 搜索案例:`search_posts(query="PDF table extraction usercase site:github.com")`
+- 搜索定义:`search_posts(query="PDF table extraction definition")`
+- 搜索方法:`search_posts(query="PDF table extraction best practice")`
+
+**Step 3: 结构化记录**
+每发现一条有价值的信息,立即保存为结构化知识:
+
+```python
+knowledge_save(
+    task="在什么情景下,要完成什么目标,得到能达成一个什么结果",
+    content="这条知识实际的核心内容",
+    types=["tool"],  # tool/usecase/definition/plan/strategy/user_profile
+    urls=["参考的论文/github/博客等"],
+    agent_id="当前 agent ID",
+    score=5
+)
+```
+
+**Step 4: 输出知识列表**
+
+```
+goal(done="已完成调研,共记录 N 条知识")
+```
+
+### 输出
+
+- 保存到 `.cache/knowledge_atoms/` 目录,每条知识一个 JSON 文件
+- 文件名格式:`atom-YYYYMMDD-HHMMSS-XXXX.json`
+- 返回知识列表摘要
+
+---
+
+## 知识结构
+
+每条知识原子包含以下字段:
+
+### ① id(唯一标识)
+
+格式:`atom-YYYYMMDD-NNN`
+
+示例:`atom-20260302-001`
+
+### ② tags(知识类型)
+
+单条知识可以包含多个标签:
+
+| 类型           | 说明                                       | 示例                                |
+| -------------- | ------------------------------------------ | ----------------------------------- |
+| **tool**       | 工具相关信息,包含使用案例和使用方法       | pdfplumber 库的使用方法             |
+| **usercase**   | 针对任务的用户案例,某个用户完成任务的方法 | 某用户如何提取 PDF 表格的完整流程   |
+| **definition** | 内容的具体定义,或任务的问题定义           | 什么是 PDF 表格提取,有哪些技术挑战 |
+| **plan**       | 完成任务的通用计划、方法论或流程           | PDF 表格提取的标准流程和最佳实践    |
+
+### ③ summary(摘要)
+
+**格式**:在 [情景] 下,完成 [目标],得到 [预期结果]。
+
+**示例**:
+
+```
+在 Python 3.11 环境下,需要从结构复杂的 PDF(包含多列、嵌套表格)中提取表格数据,
+并保留单元格坐标信息,最终得到可用于数据分析的结构化数据。
+```
+
+### ④ content(核心内容)
+
+这条知识实际的核心内容,使用 Markdown 格式,根据类型不同包含不同信息:
+
+**tool 类型**:
+
+- 工具名称和简介
+- 核心 API 和使用方法
+- 适用场景
+- 优缺点对比
+- 代码示例
+
+**usercase 类型**:
+
+- 用户背景和需求
+- 采用的方案
+- 实现步骤
+- 遇到的问题和解决方法
+- 最终效果
+
+**definition 类型**:
+
+- 概念定义
+- 技术原理
+- 应用场景
+- 与相关概念的区别
+
+**plan 类型**:
+
+- 完整流程步骤
+- 关键决策点
+- 常见陷阱和避坑指南
+- 评估标准
+
+### ⑤ tips(避坑指南)
+
+⚠️ 具体的避坑指南或核心建议。
+
+**示例**:
+
+```
+⚠️ 如果 PDF 包含隐形表格线,务必开启 explicit_horizontal_lines 参数
+⚠️ 使用 page.crop() 先裁剪区域再提取,可提升 3-5 倍速度
+```
+
+### ⑥ trace(回溯)
+
+**包含**:
+
+- `urls`: 参考的论文、GitHub、博客等(URL 列表)
+- `agent_id`: 执行此调研的 agent ID
+- `timestamp`: 记录时间
+
+**示例**:
+
+```json
+{
+  "urls": ["https://github.com/jsvine/pdfplumber"],
+  "agent_id": "research_agent_001",
+  "timestamp": "2026-03-02T12:45:41Z"
+}
+```
+
+### ⑦ eval(评估反馈)
+
+**包含**:
+
+- `helpful`: 好用的使用次数(初始值为 1)
+- `harmful`: 不好用的使用次数(初始值为 0)
+
+**示例**:
+
+```json
+{
+  "helpful": 1,
+  "harmful": 0
+}
+```
+
+### ⑧ execute_history(执行历史)
+
+**包含**:
+
+- `helpful`: 好用的使用案例描述列表(字符串数组,初始为空)
+- `harmful`: 不好用的使用案例描述列表(字符串数组,初始为空)
+
+**示例**:
+
+```json
+{
+  "helpful": [],
+  "harmful": []
+}
+```
+
+---
+
+## 完整示例
+
+````json
+{
+  "research_report": {
+    "goal": "找到 PDF 表格提取的最佳方案",
+    "task": "从复杂 PDF 中提取表格数据",
+    "summary": "共记录 3 条核心知识原子,涵盖工具选型与实战 SOP",
+    "atoms": [
+      {
+        "id": "atom-20260302-001",
+        "tags": ["tool", "plan", "usercase"],
+        "summary": "在 Python 3.11 环境下,从结构复杂的 PDF(包含多列、嵌套表格)中提取表格数据,并保留单元格坐标信息,最终得到可用于数据分析的结构化数据。",
+        "content": "## 推荐工具\npdfplumber - 专注于 PDF 表格提取\n\n## 核心 API\n使用 extract_tables() 方法\n\n## 代码示例\n```python\nimport pdfplumber\nwith pdfplumber.open('file.pdf') as pdf:\n    tables = pdf.pages[0].extract_tables()\n```",
+        "tips": "⚠️ 如果 PDF 包含隐形表格线,务必开启 explicit_horizontal_lines 参数\n⚠️ 使用 page.crop() 先裁剪区域再提取,可提升 3-5 倍速度",
+        "trace": {
+          "urls": ["https://github.com/jsvine/pdfplumber"],
+          "agent_id": "research_agent_001",
+          "timestamp": "2026-03-02T12:45:41Z"
+        },
+        "eval": {
+          "helpful": 1,
+          "harmful": 0
+        },
+        "execute_history": {
+          "helpful": [],
+          "harmful": []
+        }
+      },
+      {
+        "id": "atom-20260302-002",
+        "tags": ["usercase"],
+        "summary": "针对 500MB 以上的大型扫描版 PDF 进行自动化处理。",
+        "content": "使用纯 Python 库(如 pdfplumber/PyMuPDF)性能极差且准确率低。",
+        "tips": "建议方案:先使用 PaddleOCR 进行版面分析,再提取坐标区域。",
+        "trace": {
+          "urls": ["https://reddit.com/r/python/comments/..."],
+          "agent_id": "research_agent_01",
+          "timestamp": "2026-03-02T13:00:00Z"
+        },
+        "eval": {
+          "helpful": 1,
+          "harmful": 0
+        },
+        "execute_history": {
+          "helpful": [],
+          "harmful": []
+        }
+      }
+    ]
+  }
+}
+````
+
+---
+
+## 适用场景
+
+- 需要从多个来源收集和整理知识
+- 需要持续积累和评估知识的有效性
+- 需要追溯知识来源和使用历史
+
+---
+
+## 使用工具
+
+### 保存知识
+
+````python
+knowledge_save(
+    task="在 Python 3.11 环境下,从复杂 PDF 中提取表格数据,并保留单元格坐标信息。",
+    content="""
+## 推荐工具
+pdfplumber - 专注于 PDF 表格提取
+
+## 核心 API
+使用 extract_tables() 方法
+
+## 代码示例
+```python
+import pdfplumber
+with pdfplumber.open('file.pdf') as pdf:
+    tables = pdf.pages[0].extract_tables()
+```
+
+⚠️ 必须设置 explicit_horizontal_lines=True 以识别隐形表格线
+""",
+    types=["tool", "plan"],
+    urls=["https://github.com/jsvine/pdfplumber"],
+    agent_id="research_agent_001",
+    score=5
+)
+````
+
+### 更新评估反馈
+
+```python
+knowledge_update(
+    knowledge_id="knowledge-20260302-001",
+    add_helpful_case={
+        "description": "在解析 2025 年报 PDF 时,通过配置 explicit_lines 成功提取了 100+ 嵌套表格。",
+        "trace_id": "trace-xxx"
+    }
+)
+```
+
+或添加失败案例:
+
+```python
+knowledge_update(
+    knowledge_id="knowledge-20260302-001",
+    add_harmful_case={
+        "description": "在处理 300MB 的扫描版 PDF 时,该方案因缺乏 OCR 能力导致提取结果为空。",
+        "trace_id": "trace-xxx"
+    }
+)
+```
+
+---
+
+## 调研策略
+
+### 1. 工具调研(tool)
+
+**搜索关键词**:
+
+- `[任务] tool python`
+- `[任务] library comparison`
+- `[任务] vs site:reddit.com`
+
+**记录重点**:
+
+- 工具名称和链接
+- 核心 API
+- 适用场景
+- 优缺点对比
+- 避坑指南
+
+### 2. 案例调研(usercase)
+
+**搜索关键词**:
+
+- `[任务] example site:github.com`
+- `[任务] tutorial`
+- `how to [任务]`
+
+**记录重点**:
+
+- 用户背景
+- 采用方案
+- 实现步骤
+- 遇到的问题
+- 最终效果
+
+### 3. 定义调研(definition)
+
+**搜索关键词**:
+
+- `what is [任务]`
+- `[任务] definition`
+- `[任务] explained`
+
+**记录重点**:
+
+- 概念定义
+- 技术原理
+- 应用场景
+- 相关概念区别
+
+### 4. 方法论调研(plan)
+
+**搜索关键词**:
+
+- `[任务] best practice`
+- `[任务] workflow`
+- `[任务] step by step`
+
+**记录重点**:
+
+- 完整流程
+- 关键决策点
+- 常见陷阱
+- 评估标准
+
+---
+
+## 输出格式
+
+调研完成后,输出知识列表摘要:
+
+```
+📚 调研完成报告
+
+目标:找到 PDF 表格提取的最佳方案
+任务:从复杂 PDF 中提取表格数据
+
+共记录 5 条知识:
+
+1. [tool] pdfplumber - PDF 表格提取工具
+   评分:⭐⭐⭐⭐⭐ (5/5)
+   反馈:helpful: 1, harmful: 0
+
+2. [usercase] 财报 PDF 表格提取案例
+   评分:⭐⭐⭐⭐ (4/5)
+   反馈:helpful: 1, harmful: 0
+
+3. [definition] PDF 表格提取技术原理
+   评分:⭐⭐⭐⭐ (4/5)
+   反馈:helpful: 1, harmful: 0
+
+4. [plan] PDF 表格提取标准流程
+   评分:⭐⭐⭐⭐⭐ (5/5)
+   反馈:helpful: 1, harmful: 0
+
+5. [tool] tabula-py - 替代方案
+   评分:⭐⭐⭐ (3/5)
+   反馈:helpful: 1, harmful: 0
+
+知识文件保存在:.cache/knowledge_atoms/
+```
+
+---
+
+## 记住
+
+- **边搜边记**:发现有价值的信息立即保存
+- **结构化**:严格按照 5 个维度记录
+- **可追溯**:记录所有参考来源
+- **可评估**:初始评分 + 持续反馈

+ 8 - 4
agent/tools/builtin/__init__.py

@@ -15,12 +15,11 @@ from agent.tools.builtin.file.grep import grep_content
 from agent.tools.builtin.bash import bash_command
 from agent.tools.builtin.bash import bash_command
 from agent.tools.builtin.skill import skill, list_skills
 from agent.tools.builtin.skill import skill, list_skills
 from agent.tools.builtin.subagent import agent, evaluate
 from agent.tools.builtin.subagent import agent, evaluate
-from agent.tools.builtin.experience import get_experience
 from agent.tools.builtin.search import search_posts, get_search_suggestions
 from agent.tools.builtin.search import search_posts, get_search_suggestions
 from agent.tools.builtin.sandbox import (sandbox_create_environment, sandbox_run_shell,
 from agent.tools.builtin.sandbox import (sandbox_create_environment, sandbox_run_shell,
                                          sandbox_rebuild_with_ports,sandbox_destroy_environment)
                                          sandbox_rebuild_with_ports,sandbox_destroy_environment)
+from agent.tools.builtin.knowledge import(knowledge_search,knowledge_save,knowledge_list,knowledge_update,knowledge_batch_update,knowledge_slim)
 from agent.trace.goal_tool import goal
 from agent.trace.goal_tool import goal
-
 # 导入浏览器工具以触发注册
 # 导入浏览器工具以触发注册
 import agent.tools.builtin.browser  # noqa: F401
 import agent.tools.builtin.browser  # noqa: F401
 
 
@@ -36,7 +35,12 @@ __all__ = [
     # 系统工具
     # 系统工具
     "bash_command",
     "bash_command",
     "skill",
     "skill",
-    "get_experience",
+    "knowledge_search",
+    "knowledge_save",
+    "knowledge_list",
+    "knowledge_update",
+    "knowledge_batch_update",
+    "knowledge_slim",
     "list_skills",
     "list_skills",
     "agent",
     "agent",
     "evaluate",
     "evaluate",
@@ -46,6 +50,6 @@ __all__ = [
     "sandbox_run_shell",
     "sandbox_run_shell",
     "sandbox_rebuild_with_ports",
     "sandbox_rebuild_with_ports",
     "sandbox_destroy_environment",
     "sandbox_destroy_environment",
-    # 计划管理
+    # Goal 管理
     "goal",
     "goal",
 ]
 ]

+ 162 - 27
agent/tools/builtin/bash.py

@@ -1,27 +1,139 @@
 """
 """
 Bash Tool - 命令执行工具
 Bash Tool - 命令执行工具
 
 
-参考 OpenCode bash.ts 完整实现。
-
 核心功能:
 核心功能:
 - 执行 shell 命令
 - 执行 shell 命令
 - 超时控制
 - 超时控制
 - 工作目录设置
 - 工作目录设置
 - 环境变量传递
 - 环境变量传递
+- 虚拟环境隔离(Python 命令,强制执行,LLM 不可控)
+- 目录白名单保护
 """
 """
 
 
 import os
 import os
 import signal
 import signal
 import asyncio
 import asyncio
+import logging
 from pathlib import Path
 from pathlib import Path
 from typing import Optional, Dict
 from typing import Optional, Dict
 
 
 from agent.tools import tool, ToolResult, ToolContext
 from agent.tools import tool, ToolResult, ToolContext
 
 
 # 常量
 # 常量
-DEFAULT_TIMEOUT = 120  # 2 分钟
-MAX_OUTPUT_LENGTH = 50000  # 最大输出长度
-GRACEFUL_KILL_WAIT = 3  # SIGTERM 后等几秒再 SIGKILL
+DEFAULT_TIMEOUT = 120
+MAX_OUTPUT_LENGTH = 50000
+GRACEFUL_KILL_WAIT = 3
+
+# ===== 安全配置(模块级,LLM 不可控)=====
+ENABLE_VENV = True                # 是否强制启用虚拟环境
+VENV_DIR = ".venv"                # 虚拟环境目录名(相对于项目根目录)
+
+ALLOWED_WORKDIR_PATTERNS = [
+    ".",
+    "examples",
+    "examples/**",
+    ".cache",
+    ".cache/**",
+    "tests",
+    "tests/**",
+    "output",
+    "output/**",
+]
+
+PYTHON_KEYWORDS = ["python", "python3", "pip", "pip3", "pytest", "poetry", "uv"]
+
+logger = logging.getLogger(__name__)
+
+
+def _get_project_root() -> Path:
+    """获取项目根目录(bash.py 在 agent/tools/builtin/ 下)"""
+    return Path(__file__).parent.parent.parent.parent
+
+
+def _is_safe_workdir(path: Path) -> bool:
+    """检查工作目录是否在白名单内"""
+    try:
+        project_root = _get_project_root()
+        resolved_path = path.resolve()
+        resolved_root = project_root.resolve()
+
+        if not resolved_path.is_relative_to(resolved_root):
+            return False
+
+        relative_path = resolved_path.relative_to(resolved_root)
+
+        for pattern in ALLOWED_WORKDIR_PATTERNS:
+            if pattern == ".":
+                if relative_path == Path("."):
+                    return True
+            elif pattern.endswith("/**"):
+                base = Path(pattern[:-3])
+                if relative_path == base or relative_path.is_relative_to(base):
+                    return True
+            else:
+                if relative_path == Path(pattern):
+                    return True
+
+        return False
+    except (ValueError, OSError) as e:
+        logger.warning(f"路径检查失败: {e}")
+        return False
+
+
+def _should_use_venv(command: str) -> bool:
+    """判断命令是否应该在虚拟环境中执行"""
+    command_lower = command.lower()
+    for keyword in PYTHON_KEYWORDS:
+        if keyword in command_lower:
+            return True
+    return False
+
+
+async def _ensure_venv(venv_path: Path) -> bool:
+    """确保虚拟环境存在,不存在则创建"""
+    if os.name == 'nt':
+        python_exe = venv_path / "Scripts" / "python.exe"
+    else:
+        python_exe = venv_path / "bin" / "python"
+
+    if venv_path.exists() and python_exe.exists():
+        return True
+
+    # 创建虚拟环境
+    print(f"[bash] 正在创建虚拟环境: {venv_path}")
+    logger.info(f"创建虚拟环境: {venv_path}")
+    try:
+        process = await asyncio.create_subprocess_shell(
+            f"python -m venv {venv_path}",
+            stdout=asyncio.subprocess.PIPE,
+            stderr=asyncio.subprocess.PIPE,
+        )
+        stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=60)
+
+        if process.returncode == 0:
+            print(f"[bash] ✅ 虚拟环境创建成功: {venv_path}")
+            logger.info(f"虚拟环境创建成功: {venv_path}")
+            return True
+        else:
+            err_text = stderr.decode('utf-8', errors='replace') if stderr else ""
+            print(f"[bash] ❌ 虚拟环境创建失败: {err_text[:200]}")
+            logger.error(f"虚拟环境创建失败: exit code {process.returncode}, {err_text[:200]}")
+            return False
+
+    except Exception as e:
+        print(f"[bash] ❌ 虚拟环境创建异常: {e}")
+        logger.error(f"虚拟环境创建异常: {e}")
+        return False
+
+
+def _wrap_command_with_venv(command: str, venv_path: Path) -> str:
+    """将命令包装为在虚拟环境中执行"""
+    if os.name == 'nt':
+        activate_script = venv_path / "Scripts" / "activate.bat"
+        return f'call "{activate_script}" && {command}'
+    else:
+        activate_script = venv_path / "bin" / "activate"
+        return f'source "{activate_script}" && {command}'
 
 
 
 
 def _kill_process_tree(pid: int) -> None:
 def _kill_process_tree(pid: int) -> None:
@@ -33,23 +145,20 @@ def _kill_process_tree(pid: int) -> None:
     except ProcessLookupError:
     except ProcessLookupError:
         return
         return
 
 
-    # 先优雅终止
     try:
     try:
         os.killpg(pgid, signal.SIGTERM)
         os.killpg(pgid, signal.SIGTERM)
     except ProcessLookupError:
     except ProcessLookupError:
         return
         return
 
 
-    # 等一小段时间让进程自行退出
     time.sleep(GRACEFUL_KILL_WAIT)
     time.sleep(GRACEFUL_KILL_WAIT)
 
 
-    # 强制杀
     try:
     try:
         os.killpg(pgid, signal.SIGKILL)
         os.killpg(pgid, signal.SIGKILL)
     except ProcessLookupError:
     except ProcessLookupError:
-        pass  # 已退出
+        pass
 
 
 
 
-@tool(description="执行 bash 命令")
+@tool(description="执行 bash 命令", hidden_params=["context"])
 async def bash_command(
 async def bash_command(
     command: str,
     command: str,
     timeout: Optional[int] = None,
     timeout: Optional[int] = None,
@@ -72,7 +181,6 @@ async def bash_command(
     Returns:
     Returns:
         ToolResult: 命令输出
         ToolResult: 命令输出
     """
     """
-    # 参数验证
     if timeout is not None and timeout < 0:
     if timeout is not None and timeout < 0:
         return ToolResult(
         return ToolResult(
             title="参数错误",
             title="参数错误",
@@ -91,6 +199,32 @@ async def bash_command(
             error="Directory not found"
             error="Directory not found"
         )
         )
 
 
+    # 目录白名单检查
+    if not _is_safe_workdir(cwd):
+        project_root = _get_project_root()
+        return ToolResult(
+            title="目录不允许",
+            output=(
+                f"工作目录不在白名单内: {cwd}\n"
+                f"项目根目录: {project_root}\n"
+                f"允许的目录: {', '.join(ALLOWED_WORKDIR_PATTERNS)}"
+            ),
+            error="Directory not allowed"
+        )
+
+    # 虚拟环境处理(强制执行,LLM 不可绕过)
+    actual_command = command
+    if ENABLE_VENV and _should_use_venv(command):
+        venv_dir = _get_project_root() / VENV_DIR
+
+        venv_ok = await _ensure_venv(venv_dir)
+        if venv_ok:
+            actual_command = _wrap_command_with_venv(command, venv_dir)
+            print(f"[bash] 🐍 使用虚拟环境: {venv_dir}")
+            logger.info(f"[bash] 使用虚拟环境: {venv_dir}")
+        else:
+            logger.warning(f"[bash] 虚拟环境不可用,回退到系统环境: {venv_dir}")
+
     # 准备环境变量
     # 准备环境变量
     process_env = os.environ.copy()
     process_env = os.environ.copy()
     if env:
     if env:
@@ -98,23 +232,30 @@ async def bash_command(
 
 
     # 执行命令
     # 执行命令
     try:
     try:
-        process = await asyncio.create_subprocess_shell(
-            command,
-            stdout=asyncio.subprocess.PIPE,
-            stderr=asyncio.subprocess.PIPE,
-            cwd=str(cwd),
-            env=process_env,
-            start_new_session=True,  # 新进程组,超时时可杀整棵进程树
-        )
+        if os.name == 'nt':
+            process = await asyncio.create_subprocess_shell(
+                actual_command,
+                stdout=asyncio.subprocess.PIPE,
+                stderr=asyncio.subprocess.PIPE,
+                cwd=str(cwd),
+                env=process_env,
+            )
+        else:
+            process = await asyncio.create_subprocess_shell(
+                actual_command,
+                stdout=asyncio.subprocess.PIPE,
+                stderr=asyncio.subprocess.PIPE,
+                cwd=str(cwd),
+                env=process_env,
+                start_new_session=True,
+            )
 
 
-        # 等待命令完成(带超时)
         try:
         try:
             stdout, stderr = await asyncio.wait_for(
             stdout, stderr = await asyncio.wait_for(
                 process.communicate(),
                 process.communicate(),
                 timeout=timeout_sec
                 timeout=timeout_sec
             )
             )
         except asyncio.TimeoutError:
         except asyncio.TimeoutError:
-            # 超时,杀整个进程组(shell + 所有子进程)
             _kill_process_tree(process.pid)
             _kill_process_tree(process.pid)
             try:
             try:
                 await asyncio.wait_for(process.wait(), timeout=GRACEFUL_KILL_WAIT + 2)
                 await asyncio.wait_for(process.wait(), timeout=GRACEFUL_KILL_WAIT + 2)
@@ -127,30 +268,24 @@ async def bash_command(
                 metadata={"command": command, "timeout": timeout_sec}
                 metadata={"command": command, "timeout": timeout_sec}
             )
             )
 
 
-        # 解码输出
         stdout_text = stdout.decode('utf-8', errors='replace') if stdout else ""
         stdout_text = stdout.decode('utf-8', errors='replace') if stdout else ""
         stderr_text = stderr.decode('utf-8', errors='replace') if stderr else ""
         stderr_text = stderr.decode('utf-8', errors='replace') if stderr else ""
 
 
-        # 截断过长输出
         truncated = False
         truncated = False
         if len(stdout_text) > MAX_OUTPUT_LENGTH:
         if len(stdout_text) > MAX_OUTPUT_LENGTH:
             stdout_text = stdout_text[:MAX_OUTPUT_LENGTH] + f"\n\n(输出被截断,总长度: {len(stdout_text)} 字符)"
             stdout_text = stdout_text[:MAX_OUTPUT_LENGTH] + f"\n\n(输出被截断,总长度: {len(stdout_text)} 字符)"
             truncated = True
             truncated = True
 
 
-        # 组合输出
         output = ""
         output = ""
         if stdout_text:
         if stdout_text:
             output += stdout_text
             output += stdout_text
-
         if stderr_text:
         if stderr_text:
             if output:
             if output:
                 output += "\n\n--- stderr ---\n"
                 output += "\n\n--- stderr ---\n"
             output += stderr_text
             output += stderr_text
-
         if not output:
         if not output:
             output = "(命令无输出)"
             output = "(命令无输出)"
 
 
-        # 检查退出码
         exit_code = process.returncode
         exit_code = process.returncode
         success = exit_code == 0
         success = exit_code == 0
 
 

+ 4 - 0
agent/tools/builtin/browser/__init__.py

@@ -8,10 +8,12 @@ from agent.tools.builtin.browser.baseClass import (
     # 会话管理
     # 会话管理
     init_browser_session,
     init_browser_session,
     get_browser_session,
     get_browser_session,
+    get_browser_live_url,
     cleanup_browser_session,
     cleanup_browser_session,
     kill_browser_session,
     kill_browser_session,
 
 
     # 导航类工具
     # 导航类工具
+    browser_get_live_url,
     browser_navigate_to_url,
     browser_navigate_to_url,
     browser_search_web,
     browser_search_web,
     browser_go_back,
     browser_go_back,
@@ -63,10 +65,12 @@ __all__ = [
     # 会话管理
     # 会话管理
     'init_browser_session',
     'init_browser_session',
     'get_browser_session',
     'get_browser_session',
+    'get_browser_live_url',
     'cleanup_browser_session',
     'cleanup_browser_session',
     'kill_browser_session',
     'kill_browser_session',
 
 
     # 导航类工具
     # 导航类工具
+    'browser_get_live_url',
     'browser_navigate_to_url',
     'browser_navigate_to_url',
     'browser_search_web',
     'browser_search_web',
     'browser_go_back',
     'browser_go_back',

+ 131 - 8
agent/tools/builtin/browser/baseClass.py

@@ -62,6 +62,9 @@ from ....llm.openrouter import openrouter_llm_call
 # 将项目根目录添加到 Python 路径
 # 将项目根目录添加到 Python 路径
 sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
 sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
 
 
+# 配置日志
+logger = logging.getLogger(__name__)
+
 # 导入框架的工具装饰器和结果类
 # 导入框架的工具装饰器和结果类
 from agent.tools import tool, ToolResult
 from agent.tools import tool, ToolResult
 from agent.tools.builtin.browser.sync_mysql_help import mysql
 from agent.tools.builtin.browser.sync_mysql_help import mysql
@@ -95,6 +98,9 @@ from browser_use.filesystem.file_system import FileSystem
 _browser_session: Optional[BrowserSession] = None
 _browser_session: Optional[BrowserSession] = None
 _browser_tools: Optional[Tools] = None
 _browser_tools: Optional[Tools] = None
 _file_system: Optional[FileSystem] = None
 _file_system: Optional[FileSystem] = None
+_last_browser_type: str = "local"
+_last_headless: bool = True
+_live_url: Optional[str] = None
 
 
 async def create_container(url: str, account_name: str = "liuwenwu") -> Dict[str, Any]:
 async def create_container(url: str, account_name: str = "liuwenwu") -> Dict[str, Any]:
     """
     """
@@ -234,11 +240,14 @@ async def init_browser_session(
     browser_profile: Optional[BrowserProfile] = None,
     browser_profile: Optional[BrowserProfile] = None,
     **kwargs
     **kwargs
 ) -> tuple[BrowserSession, Tools]:
 ) -> tuple[BrowserSession, Tools]:
-    global _browser_session, _browser_tools, _file_system
+    global _browser_session, _browser_tools, _file_system, _last_browser_type, _last_headless, _live_url
 
 
     if _browser_session is not None:
     if _browser_session is not None:
         return _browser_session, _browser_tools
         return _browser_session, _browser_tools
 
 
+    _last_browser_type = browser_type
+    _last_headless = headless
+
     valid_types = ["local", "cloud", "container"]
     valid_types = ["local", "cloud", "container"]
     if browser_type not in valid_types:
     if browser_type not in valid_types:
         raise ValueError(f"无效的 browser_type: {browser_type}")
         raise ValueError(f"无效的 browser_type: {browser_type}")
@@ -300,12 +309,28 @@ async def init_browser_session(
 
 
     print(f"✅ 浏览器会话初始化成功 | 默认下载路径: {save_dir}")
     print(f"✅ 浏览器会话初始化成功 | 默认下载路径: {save_dir}")
 
 
+    # 云浏览器:捕获 live URL
+    if browser_type == "cloud":
+        import urllib.parse
+        cdp_url = getattr(_browser_session, 'cdp_url', '') or ''
+        if 'browser-use.com' in cdp_url:
+            # 从 cdp_url (wss://xxx.cdp1.browser-use.com/...) 提取主机名,用 https:// 拼接
+            parsed = urllib.parse.urlparse(cdp_url)
+            host_url = f"https://{parsed.hostname}"
+            _live_url = f"https://live.browser-use.com?wss={urllib.parse.quote(host_url)}"
+            print(f"📡 实时画面链接: {_live_url}")
+
     if browser_type in ["local", "cloud"] and url:
     if browser_type in ["local", "cloud"] and url:
         await _browser_tools.navigate(url=url, browser_session=_browser_session)
         await _browser_tools.navigate(url=url, browser_session=_browser_session)
 
 
     return _browser_session, _browser_tools
     return _browser_session, _browser_tools
 
 
 
 
+def get_browser_live_url() -> Optional[str]:
+    """获取云浏览器的实时画面链接"""
+    return _live_url
+
+
 async def get_browser_session() -> tuple[BrowserSession, Tools]:
 async def get_browser_session() -> tuple[BrowserSession, Tools]:
     """
     """
     获取当前浏览器会话,如果不存在或连接已断开则自动重新创建
     获取当前浏览器会话,如果不存在或连接已断开则自动重新创建
@@ -346,7 +371,7 @@ async def get_browser_session() -> tuple[BrowserSession, Tools]:
                 _file_system = None
                 _file_system = None
 
 
     if _browser_session is None:
     if _browser_session is None:
-        await init_browser_session()
+        await init_browser_session(browser_type=_last_browser_type, headless=_last_headless)
 
 
     return _browser_session, _browser_tools
     return _browser_session, _browser_tools
 
 
@@ -527,6 +552,25 @@ def _fetch_profile_id(cookie_type: str) -> Optional[str]:
 # 导航类工具 (Navigation Tools)
 # 导航类工具 (Navigation Tools)
 # ============================================================
 # ============================================================
 
 
+@tool()
+async def browser_get_live_url() -> ToolResult:
+    """
+    获取云浏览器的实时画面链接(Live URL),可用于在本地浏览器中查看或分享给他人操作。
+    仅在云浏览器模式下有效,本地浏览器返回空。
+    """
+    url = get_browser_live_url()
+    if url:
+        return ToolResult(
+            title="云浏览器实时画面链接",
+            output=url,
+            metadata={"live_url": url}
+        )
+    return ToolResult(
+        title="无可用链接",
+        output="当前未使用云浏览器,或浏览器尚未初始化",
+    )
+
+
 @tool()
 @tool()
 async def browser_navigate_to_url(url: str, new_tab: bool = False) -> ToolResult:
 async def browser_navigate_to_url(url: str, new_tab: bool = False) -> ToolResult:
     """
     """
@@ -1140,7 +1184,7 @@ async def browser_screenshot() -> ToolResult:
     try:
     try:
         browser, tools = await get_browser_session()
         browser, tools = await get_browser_session()
 
 
-        result = await tools.screenshot(browser_session=browser)
+        result = await tools.screenshot(browser_session=browser, file_system=_file_system)
 
 
         return action_result_to_tool_result(result, "截图请求")
         return action_result_to_tool_result(result, "截图请求")
 
 
@@ -2003,14 +2047,17 @@ async def browser_export_cookies(name: str = "", account: str = "") -> ToolResul
 
 
 
 
 @tool()
 @tool()
-async def browser_load_cookies(url: str, name: str = "") -> ToolResult:
+async def browser_load_cookies(url: str, name: str = "", auto_navigate: bool = True) -> ToolResult:
     """
     """
     根据目标 URL 自动查找本地 Cookie 文件,注入浏览器并导航到目标页面恢复登录态。
     根据目标 URL 自动查找本地 Cookie 文件,注入浏览器并导航到目标页面恢复登录态。
+    如果找不到 Cookie 文件,会根据 auto_navigate 参数决定是否直接导航到目标页面。
+
     重要:此工具会自动完成导航,调用前不需要先调用 browser_navigate_to_url。
     重要:此工具会自动完成导航,调用前不需要先调用 browser_navigate_to_url。
 
 
     Args:
     Args:
         url: 目标 URL(必须提供,同时用于自动匹配 Cookie 文件)
         url: 目标 URL(必须提供,同时用于自动匹配 Cookie 文件)
         name: Cookie 文件名(可选,不传则根据 URL 域名自动查找)
         name: Cookie 文件名(可选,不传则根据 URL 域名自动查找)
+        auto_navigate: 找不到 Cookie 时是否自动导航到目标页面(默认 True)
     """
     """
     try:
     try:
         browser, tools = await get_browser_session()
         browser, tools = await get_browser_session()
@@ -2023,19 +2070,95 @@ async def browser_load_cookies(url: str, name: str = "") -> ToolResult:
             from urllib.parse import urlparse
             from urllib.parse import urlparse
             domain = urlparse(url).netloc.replace("www.", "")
             domain = urlparse(url).netloc.replace("www.", "")
             if _COOKIES_DIR.exists():
             if _COOKIES_DIR.exists():
-                matches = list(_COOKIES_DIR.glob(f"{domain}*.json"))
+                # 尝试多种匹配模式
+                matches = []
+
+                # 1. 精确匹配完整域名(如 xiaohongshu.com.json)
+                exact_match = _COOKIES_DIR / f"{domain}.json"
+                if exact_match.exists():
+                    matches.append(exact_match)
+                    logger.info(f"Cookie 精确匹配成功: {exact_match.name}")
+
+                # 2. 匹配域名前缀(如 xiaohongshu.com*.json)
+                if not matches:
+                    prefix_matches = list(_COOKIES_DIR.glob(f"{domain}*.json"))
+                    if prefix_matches:
+                        matches = prefix_matches
+                        logger.info(f"Cookie 前缀匹配成功: {[m.name for m in matches]}")
+
+                # 3. 模糊匹配:提取主域名(如 xiaohongshu)
+                if not matches:
+                    main_domain = domain.split('.')[0]  # 提取第一部分
+                    fuzzy_matches = list(_COOKIES_DIR.glob(f"{main_domain}*.json"))
+                    if fuzzy_matches:
+                        matches = fuzzy_matches
+                        logger.info(f"Cookie 模糊匹配成功: {[m.name for m in matches]} (主域名: {main_domain})")
+
                 if matches:
                 if matches:
                     cookie_file = matches[0]  # 取第一个匹配的
                     cookie_file = matches[0]  # 取第一个匹配的
+                    logger.info(f"使用 Cookie 文件: {cookie_file.name}")
                 else:
                 else:
                     available = [f.stem for f in _COOKIES_DIR.glob("*.json")]
                     available = [f.stem for f in _COOKIES_DIR.glob("*.json")]
-                    return ToolResult(title="未找到 Cookie", output=f"没有匹配 {domain} 的文件,可用: {available}", error=f"无 {domain} 的 Cookie 文件")
+                    logger.warning(f"未找到匹配的 Cookie 文件。域名: {domain}, 可用: {available}")
+                    hint = f"可用的 Cookie 文件: {available}" if available else "提示:首次使用需要先手动登录,然后使用 browser_export_cookies 保存 Cookie"
+
+                    # 如果启用自动导航,直接访问目标页面
+                    if auto_navigate:
+                        await tools.navigate(url=url, browser_session=browser)
+                        await tools.wait(seconds=2, browser_session=browser)
+                        return ToolResult(
+                            title="未找到 Cookie,已导航到目标页面",
+                            output=f"没有找到 {domain} 的 Cookie 文件,已自动导航到 {url}。\n\n{hint}\n\n建议:如需保持登录态,请手动登录后使用 browser_export_cookies 保存 Cookie。",
+                            error=None,
+                            long_term_memory=f"未找到 {domain} 的 Cookie,已导航到 {url}"
+                        )
+                    else:
+                        return ToolResult(
+                            title="未找到 Cookie",
+                            output=f"没有匹配 {domain} 的 Cookie 文件。{hint}\n\n建议:使用 browser_navigate_to_url 访问 {url} 并手动登录,或使用 browser_export_cookies 保存当前 Cookie。",
+                            error=None,
+                            long_term_memory=f"未找到 {domain} 的 Cookie 文件"
+                        )
             else:
             else:
-                return ToolResult(title="未找到 Cookie", output=".cookies 目录不存在", error="Cookie 目录不存在")
+                # Cookie 目录不存在
+                if auto_navigate:
+                    await tools.navigate(url=url, browser_session=browser)
+                    await tools.wait(seconds=2, browser_session=browser)
+                    return ToolResult(
+                        title="首次使用 Cookie 功能,已导航到目标页面",
+                        output=f"这是首次使用 Cookie 功能,已自动导航到 {url}。\n\n建议:手动完成登录后,使用 browser_export_cookies 保存 Cookie 供下次使用。",
+                        error=None,
+                        long_term_memory="首次使用 Cookie 功能,已导航到目标页面"
+                    )
+                else:
+                    return ToolResult(
+                        title="Cookie 目录不存在",
+                        output=f"这是首次使用 Cookie 功能。建议:\n1. 使用 browser_navigate_to_url 访问 {url}\n2. 手动完成登录\n3. 使用 browser_export_cookies 保存 Cookie 供下次使用",
+                        error=None,
+                        long_term_memory="Cookie 目录不存在,这是首次使用"
+                    )
         else:
         else:
             cookie_file = _COOKIES_DIR / f"{name}.json"
             cookie_file = _COOKIES_DIR / f"{name}.json"
             if not cookie_file.exists():
             if not cookie_file.exists():
                 available = [f.stem for f in _COOKIES_DIR.glob("*.json")] if _COOKIES_DIR.exists() else []
                 available = [f.stem for f in _COOKIES_DIR.glob("*.json")] if _COOKIES_DIR.exists() else []
-                return ToolResult(title="文件不存在", output=f"可用: {available}", error=f"未找到 .cookies/{name}.json")
+                hint = f"可用的 Cookie 文件: {available}" if available else "提示:使用 browser_export_cookies 保存 Cookie"
+
+                if auto_navigate:
+                    await tools.navigate(url=url, browser_session=browser)
+                    await tools.wait(seconds=2, browser_session=browser)
+                    return ToolResult(
+                        title="Cookie 文件不存在,已导航到目标页面",
+                        output=f"未找到 .cookies/{name}.json,已自动导航到 {url}。\n\n{hint}",
+                        error=None,
+                        long_term_memory=f"未找到 {name}.json,已导航到目标页面"
+                    )
+                else:
+                    return ToolResult(
+                        title="Cookie 文件不存在",
+                        output=f"未找到 .cookies/{name}.json。{hint}",
+                        error=None,
+                        long_term_memory=f"未找到 {name}.json Cookie 文件"
+                    )
 
 
         cookies = json.loads(cookie_file.read_text(encoding="utf-8"))
         cookies = json.loads(cookie_file.read_text(encoding="utf-8"))
 
 

+ 0 - 487
agent/tools/builtin/experience.py

@@ -1,487 +0,0 @@
-import logging
-import os
-import yaml
-import json
-import asyncio
-import re
-from typing import List, Optional, Dict, Any
-from datetime import datetime
-from ...llm.openrouter import openrouter_llm_call
-
-logger = logging.getLogger(__name__)
-
-# 默认经验存储路径(当无法从 context 获取时使用)
-DEFAULT_EXPERIENCES_PATH = "./.cache/experiences_0.md"
-
-def _get_experiences_path(context: Optional[Any] = None) -> str:
-    """
-    从 context 中获取 experiences_path,回退到默认路径。
-
-    context 可能包含 runner 引用,从中读取配置的路径。
-    """
-    if context and isinstance(context, dict):
-        runner = context.get("runner")
-        if runner and hasattr(runner, "experiences_path"):
-            path = runner.experiences_path or DEFAULT_EXPERIENCES_PATH
-            print(f"[Experience] 使用 runner 配置的路径: {runner.experiences_path}")
-            return path
-
-    print(f"[Experience] 使用默认路径: {DEFAULT_EXPERIENCES_PATH}")
-    return DEFAULT_EXPERIENCES_PATH
-
-# ===== 经验进化重写 =====
-async def _evolve_body_with_llm(old_body: str, feedback: str) -> str:
-    """
-    使用检索级别的小模型 (Flash Lite) 执行经验进化重写。
-    """
-    prompt = f"""你是一个 AI Agent 经验库管理员。请根据反馈建议,对现有的 ACE 规范经验进行重写进化。
-
-【原经验内容】:
-{old_body}
-
-【实战反馈建议】:
-{feedback}
-
-【重写要求】:
-1. 保持 ACE 规范:当 [条件/Context] 时,应该 [动作/Action](原因:[逻辑/Reason])。
-2. 融合知识:将反馈中的避坑指南、新参数或修正后的选择逻辑融入原经验,使其更具通用性和准确性。
-3. 语言:简洁直接,使用中文。
-4. 禁止:严禁输出任何开场白、解释语或 Markdown 标题,直接返回重写后的正文。
-"""
-    try:
-        # 调用与检索路由相同的廉价模型
-        response = await openrouter_llm_call(
-            messages=[{"role": "user", "content": prompt}],
-            model="google/gemini-2.0-flash-001" 
-        )
-        
-        evolved_content = response.get("content", "").strip()
-        
-        # 简单安全校验:如果 LLM 返回太短或为空,回退到原内容+追加
-        if len(evolved_content) < 5:
-            raise ValueError("LLM output too short")
-            
-        return evolved_content
-        
-    except Exception as e:
-        logger.warning(f"小模型进化失败,采用追加模式回退: {e}")
-        timestamp = datetime.now().strftime('%Y-%m-%d')
-        return f"{old_body}\n- [Update {timestamp}]: {feedback}"
-    
-# ===== 核心挑选逻辑 =====
-
-async def _route_experiences_by_llm(query_text: str, metadata_list: List[Dict], k: int = 3) -> List[str]:
-    """
-    第一阶段:语义路由。
-    让 LLM 挑选出 2*k 个语义相关的 ID。
-    """
-    if not metadata_list:
-        return []
-
-    # 扩大筛选范围到 2*k
-    routing_k = k * 2
-    
-    routing_data = [
-        {
-            "id": m["id"],
-            "tags": m["tags"],
-            "helpful": m["metrics"]["helpful"]
-        } for m in metadata_list
-    ]
-
-    prompt = f"""
-你是一个经验检索专家。根据用户的当前意图,从下列经验元数据中挑选出最相关的最多 {routing_k} 个经验 ID。
-意图:"{query_text}"
-
-可选经验列表:
-{json.dumps(routing_data, ensure_ascii=False, indent=1)}
-
-请直接输出 ID 列表,用逗号分隔(例如: ex_01, ex_02)。若无相关项请输出 "None"。
-"""
-
-    try:
-        print(f"\n[Step 1: 语义路由] 意图: '{query_text}' | 候选总数: {len(metadata_list)} | 目标提取数: {routing_k}")
-        
-        response = await openrouter_llm_call(
-            messages=[{"role": "user", "content": prompt}],
-            model="google/gemini-2.0-flash-001" 
-        )
-        
-        content = response.get("content", "").strip()
-        selected_ids = [idx.strip() for idx in re.split(r'[,\s]+', content) if idx.strip().startswith("ex_")]
-        
-        print(f"[Step 1: 语义路由] LLM 初选 ID ({len(selected_ids)}个): {selected_ids}")
-        return selected_ids
-    except Exception as e:
-        logger.error(f"LLM 经验路由失败: {e}")
-        return []
-
-async def _get_structured_experiences(query_text: str, top_k: int = 3, context: Optional[Any] = None):
-    """
-    1. 解析物理文件
-    2. 语义路由:提取 2*k 个 ID
-    3. 质量精排:基于 Metrics 筛选出最终的 k 个
-    """
-    print(f"[Experience System]  runner.experiences_path:  {context.get('runner').experiences_path if context and context.get('runner') else None}")
-    experiences_path = _get_experiences_path(context)
-
-    if not os.path.exists(experiences_path):
-        print(f"[Experience System] 警告: 经验文件不存在 ({experiences_path})")
-        return []
-
-    with open(experiences_path, "r", encoding="utf-8") as f:
-        file_content = f.read()
-
-    # --- 阶段 1: 解析 ---
-    # 使用正则表达式匹配 YAML frontmatter 块,避免误分割
-    pattern = r'---\n(.*?)\n---\n(.*?)(?=\n---\n|\Z)'
-    matches = re.findall(pattern, file_content, re.DOTALL)
-
-    content_map = {}
-    metadata_list = []
-
-    for yaml_str, raw_body in matches:
-        try:
-            metadata = yaml.safe_load(yaml_str)
-
-            # 检查 metadata 类型
-            if not isinstance(metadata, dict):
-                logger.error(f"跳过损坏的经验块: metadata 不是 dict,而是 {type(metadata).__name__}")
-                continue
-
-            eid = metadata.get("id")
-            if not eid:
-                logger.error("跳过损坏的经验块: 缺少 id 字段")
-                continue
-
-            meta_item = {
-                "id": eid,
-                "tags": metadata.get("tags", {}),
-                "metrics": metadata.get("metrics", {"helpful": 0, "harmful": 0}),
-            }
-            metadata_list.append(meta_item)
-            content_map[eid] = {
-                "content": raw_body.strip(),
-                "metrics": meta_item["metrics"]
-            }
-        except Exception as e:
-            logger.error(f"跳过损坏的经验块: {e}")
-            continue
-
-    # --- 阶段 2: 语义路由 (取 2*k) ---
-    candidate_ids = await _route_experiences_by_llm(query_text, metadata_list, k=top_k)
-
-    # --- 阶段 3: 质量精排 (根据 Metrics 选出最终的 k) ---
-    print(f"[Step 2: 质量精排] 正在根据 Metrics 对候选经验进行打分...")
-    scored_items = []
-    
-    for eid in candidate_ids:
-        if eid in content_map:
-            item = content_map[eid]
-            metrics = item["metrics"]
-            # 计算综合分:Helpful 是正分,Harmful 是双倍惩罚扣分
-            quality_score = metrics["helpful"] - (metrics["harmful"] * 2.0)
-            
-            # 过滤门槛:如果被标记为严重有害(score < -2),直接丢弃
-            if quality_score < -2:
-                print(f"  - 剔除有害经验: {eid} (Helpful: {metrics['helpful']}, Harmful: {metrics['harmful']})")
-                continue
-                
-            scored_items.append({
-                "id": eid,
-                "content": item["content"],
-                "helpful": metrics["helpful"],
-                "quality_score": quality_score
-            })
-
-    # 按照质量分排序,质量分相同时按 helpful 排序
-    final_sorted = sorted(scored_items, key=lambda x: (x["quality_score"], x["helpful"]), reverse=True)
-    
-    # 截取最终的 top_k
-    result = final_sorted[:top_k]
-    
-    print(f"[Step 2: 质量精排] 最终选定经验: {[it['id'] for it in result]}")
-    print(f"[Experience System] 检索结束。\n")
-    return result
-
-async def _batch_update_experiences(update_map: Dict[str, Dict[str, Any]], context: Optional[Any] = None):
-    """
-    物理层:批量更新经验。
-    修正点:正确使用 new_sections 集合,确保文件结构的完整性与并发进化的同步。
-    """
-    experiences_path = _get_experiences_path(context)
-
-    if not os.path.exists(experiences_path) or not update_map:
-        return 0
-
-    with open(experiences_path, "r", encoding="utf-8") as f:
-        full_content = f.read()
-
-    # 使用正则表达式解析,避免误分割
-    pattern = r'---\n(.*?)\n---\n(.*?)(?=\n---\n|\Z)'
-    matches = re.findall(pattern, full_content, re.DOTALL)
-
-    new_entries = []
-    evolution_tasks = []
-    evolution_registry = {}  # task_idx -> entry_idx
-
-    # --- 第一阶段:处理所有块 ---
-    for yaml_str, body in matches:
-        try:
-            meta = yaml.safe_load(yaml_str)
-            if not isinstance(meta, dict):
-                logger.error(f"跳过损坏的经验块: metadata 不是 dict")
-                continue
-
-            eid = meta.get("id")
-            if not eid:
-                logger.error("跳过损坏的经验块: 缺少 id")
-                continue
-
-            if eid in update_map:
-                instr = update_map[eid]
-                action = instr.get("action")
-                feedback = instr.get("feedback")
-
-                # 处理 mixed 中间态
-                if action == "mixed":
-                    meta["metrics"]["helpful"] += 1
-                    action = "evolve"
-
-                if action == "helpful":
-                    meta["metrics"]["helpful"] += 1
-                elif action == "harmful":
-                    meta["metrics"]["harmful"] += 1
-                elif action == "evolve" and feedback:
-                    # 注册进化任务
-                    task = _evolve_body_with_llm(body.strip(), feedback)
-                    evolution_tasks.append(task)
-                    # 记录该任务对应的 entry 索引
-                    evolution_registry[len(evolution_tasks) - 1] = len(new_entries)
-                    meta["metrics"]["helpful"] += 1
-
-                meta["updated_at"] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
-
-            # 序列化并加入 new_entries
-            meta_str = yaml.dump(meta, allow_unicode=True).strip()
-            new_entries.append((meta_str, body.strip()))
-
-        except Exception as e:
-            logger.error(f"跳过损坏的经验块: {e}")
-            continue
-
-    # --- 第二阶段:并发进化 ---
-    if evolution_tasks:
-        print(f"🧬 并发处理 {len(evolution_tasks)} 条经验进化...")
-        evolved_results = await asyncio.gather(*evolution_tasks)
-
-        # 精准回填:替换对应 entry 的 body
-        for task_idx, entry_idx in evolution_registry.items():
-            meta_str, _ = new_entries[entry_idx]
-            new_entries[entry_idx] = (meta_str, evolved_results[task_idx].strip())
-
-    # --- 第三阶段:原子化写回 ---
-    final_parts = []
-    for meta_str, body in new_entries:
-        final_parts.append(f"---\n{meta_str}\n---\n{body}\n")
-
-    final_content = "\n".join(final_parts)
-    with open(experiences_path, "w", encoding="utf-8") as f:
-        f.write(final_content)
-
-    return len(update_map)
-
-# ===== 经验库瘦身 =====
-
-async def slim_experiences(model: str = "anthropic/claude-sonnet-4.5", context: Optional[Any] = None) -> str:
-    """
-    经验库瘦身:调用顶级大模型,将经验库中语义相似的经验合并精简。
-    返回瘦身报告字符串。
-    """
-    experiences_path = _get_experiences_path(context)
-
-    if not os.path.exists(experiences_path):
-        return "经验文件不存在,无需瘦身。"
-
-    with open(experiences_path, "r", encoding="utf-8") as f:
-        file_content = f.read()
-
-    # 使用正则表达式解析,避免误分割
-    pattern = r'---\n(.*?)\n---\n(.*?)(?=\n---\n|\Z)'
-    matches = re.findall(pattern, file_content, re.DOTALL)
-
-    parsed = []
-    for yaml_str, body in matches:
-        try:
-            meta = yaml.safe_load(yaml_str)
-            if not isinstance(meta, dict):
-                continue
-            parsed.append({"meta": meta, "body": body.strip()})
-        except Exception:
-            continue
-
-    if len(parsed) < 2:
-        return f"经验库仅有 {len(parsed)} 条,无需瘦身。"
-
-    # 构造发给大模型的内容
-    entries_text = ""
-    for p in parsed:
-        m = p["meta"]
-        entries_text += f"[ID: {m.get('id')}] [Tags: {m.get('tags', {})}] "
-        entries_text += f"[Metrics: {m.get('metrics', {})}]\n"
-        entries_text += f"{p['body']}\n\n"
-
-    prompt = f"""你是一个 AI Agent 经验库管理员。以下是当前经验库的全部条目,请执行瘦身操作:
-
-【任务】:
-1. 识别语义高度相似或重复的经验,将它们合并为一条更精炼、更通用的经验。
-2. 合并时保留 helpful 最高的那条的 ID 和 metrics(metrics 中 helpful/harmful 取各条之和)。
-3. 对于独立的、无重复的经验,保持原样不动。
-4. 保持 ACE 规范格式:当 [条件/Context] 时,应该 [动作/Action](原因:[逻辑/Reason])。
-
-【当前经验库】:
-{entries_text}
-
-【输出格式要求】:
-严格按以下格式输出每条经验,条目之间用 === 分隔:
-ID: <保留的id>
-TAGS: <yaml格式的tags>
-METRICS: <yaml格式的metrics>
-BODY: <合并后的经验正文>
-===
-
-最后一行输出合并报告,格式:
-REPORT: 原有 X 条,合并后 Y 条,精简了 Z 条。
-
-禁止输出任何开场白或解释。"""
-
-    try:
-        print(f"\n[经验瘦身] 正在调用 {model} 分析 {len(parsed)} 条经验...")
-        response = await openrouter_llm_call(
-            messages=[{"role": "user", "content": prompt}],
-            model=model
-        )
-        content = response.get("content", "").strip()
-        if not content:
-            return "大模型返回为空,瘦身失败。"
-
-        # 解析大模型输出,重建经验文件
-        report_line = ""
-        new_entries = []
-        blocks = [b.strip() for b in content.split("===") if b.strip()]
-
-        for block in blocks:
-            if block.startswith("REPORT:"):
-                report_line = block
-                continue
-
-            lines = block.split("\n")
-            eid, tags, metrics, body_lines = None, {}, {}, []
-            current_field = None
-            for line in lines:
-                if line.startswith("ID:"):
-                    eid = line[3:].strip()
-                    current_field = None
-                elif line.startswith("TAGS:"):
-                    try:
-                        tags = yaml.safe_load(line[5:].strip()) or {}
-                    except Exception:
-                        tags = {}
-                    current_field = None
-                elif line.startswith("METRICS:"):
-                    try:
-                        metrics = yaml.safe_load(line[8:].strip()) or {}
-                    except Exception:
-                        metrics = {"helpful": 0, "harmful": 0}
-                    current_field = None
-                elif line.startswith("BODY:"):
-                    body_lines.append(line[5:].strip())
-                    current_field = "body"
-                elif current_field == "body":
-                    body_lines.append(line)
-
-            if eid and body_lines:
-                meta = {
-                    "id": eid,
-                    "tags": tags,
-                    "metrics": metrics,
-                    "updated_at": datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
-                }
-                meta_str = yaml.dump(meta, allow_unicode=True).strip()
-                body_str = "\n".join(body_lines).strip()
-                new_entries.append(f"---\n{meta_str}\n---\n{body_str}\n")
-
-        if not new_entries:
-            return "解析大模型输出失败,经验库未修改。"
-
-        # 写回文件
-        final = "\n".join(new_entries)
-        with open(experiences_path, "w", encoding="utf-8") as f:
-            f.write(final)
-
-        result = f"瘦身完成:{len(parsed)} → {len(new_entries)} 条经验。"
-        if report_line:
-            result += f"\n{report_line}"
-        print(f"[经验瘦身] {result}")
-        return result
-
-    except Exception as e:
-        logger.error(f"经验瘦身失败: {e}")
-        return f"瘦身失败: {e}"
-
-# ===== 对外 Tool 接口 =====
-
-from agent.tools import tool, ToolContext
-
-@tool(description="通过两阶段检索获取最相关的历史经验")
-async def get_experience(query: str, k: int = 3, context: Optional[ToolContext] = None):
-    """
-    通过两阶段检索获取最相关的历史经验。
-    第一阶段语义匹配(2*k),第二阶段质量精排(k)。
-    """
-    relevant_items = await _get_structured_experiences(
-        query_text=query,
-        top_k=k,
-        context=context
-    )
-
-    if not relevant_items:
-        return "未找到足够相关的优质经验。"
-
-    return {
-        "items": relevant_items,
-        "count": len(relevant_items)
-    }
-
-@tool()
-async def update_experiences(feedback_list: List[Dict[str, Any]], context: Optional[ToolContext] = None):
-    """
-    批量反馈历史经验的有效性。
-    
-    Args:
-        feedback_list: 评价列表,每个元素包含:
-            - ex_id: (str) 经验 ID
-            - is_effective: (bool) 是否有效
-            - feedback: (str, optional) 改进建议,若有效且有建议则触发经验进化
-    """
-    if not feedback_list:
-        return "反馈列表为空。"
-
-    # 将 Agent 的输入转换为底层函数需要的映射表格式
-    update_map = {}
-    for item in feedback_list:
-        ex_id = item.get("ex_id")
-        is_effective = item.get("is_effective")
-        comment = item.get("feedback", "")
-
-        action = "helpful" if is_effective else "harmful"
-        if is_effective and comment:
-            action = "evolve"
-        
-        update_map[ex_id] = {
-            "action": action,
-            "feedback": comment
-        }
-
-    count = await _batch_update_experiences(update_map, context)
-    return f"成功同步了 {count} 条经验的反馈。感谢你的评价!"

+ 4 - 0
agent/tools/builtin/feishu/chat.py

@@ -132,6 +132,7 @@ def update_unread_count(contact_name: str, increment: int = 1, reset: bool = Fal
 # ==================== 三、@tool 工具 ====================
 # ==================== 三、@tool 工具 ====================
 
 
 @tool(
 @tool(
+    hidden_params=["context"],
     display={
     display={
         "zh": {
         "zh": {
             "name": "获取飞书联系人列表",
             "name": "获取飞书联系人列表",
@@ -158,6 +159,7 @@ async def feishu_get_contact_list(context: Optional[ToolContext] = None) -> Tool
     )
     )
 
 
 @tool(
 @tool(
+    hidden_params=["context"],
     display={
     display={
         "zh": {
         "zh": {
             "name": "给飞书联系人发送消息",
             "name": "给飞书联系人发送消息",
@@ -280,6 +282,7 @@ async def feishu_send_message_to_contact(
         return ToolResult(title="发送异常", output=str(e), error=str(e))
         return ToolResult(title="发送异常", output=str(e), error=str(e))
 
 
 @tool(
 @tool(
+    hidden_params=["context"],
     display={
     display={
         "zh": {
         "zh": {
             "name": "获取飞书联系人回复",
             "name": "获取飞书联系人回复",
@@ -395,6 +398,7 @@ def _convert_feishu_msg_to_openai_content(client: FeishuClient, msg: Dict[str, A
     return blocks
     return blocks
 
 
 @tool(
 @tool(
+    hidden_params=["context"],
     display={
     display={
         "zh": {
         "zh": {
             "name": "获取飞书聊天历史记录",
             "name": "获取飞书聊天历史记录",

+ 5 - 0
agent/tools/builtin/feishu/chat_history/chat_summary.json

@@ -0,0 +1,5 @@
+{
+  "关涛": 0,
+  "谭景玉": 0,
+  "孙若天": 0
+}

+ 60 - 0
agent/tools/builtin/feishu/chat_history/chat_关涛.json

@@ -0,0 +1,60 @@
+[
+  {
+    "role": "assistant",
+    "message_id": "om_x100b55dc966810acc4cc4e7e2d5622a",
+    "content": [
+      {
+        "type": "text",
+        "text": "你好,我需要你帮忙登录一下小红书。\n\n请点击下面的链接,在浏览器中完成登录操作:\nhttps://live.browser-use.com?wss=wss%3A//ccc57863-bba1-457f-80eb-219046165133.cdp1.browser-use.com/devtools/browser/d451a1dc-4476-4084-bc4f-19244c946193\n\n登录完成后请告诉我一声,谢谢!"
+      }
+    ]
+  },
+  {
+    "role": "assistant",
+    "message_id": "om_x100b55dd5d9c00b4c3beef370384365",
+    "content": [
+      {
+        "type": "text",
+        "text": "你好,我在尝试访问小红书搜索摄影主题时遇到了登录问题。之前加载的Cookie似乎已经失效,系统一直弹出登录窗口要求重新登录。需要你帮忙处理一下小红书的登录认证问题。"
+      }
+    ]
+  },
+  {
+    "role": "assistant",
+    "message_id": "om_x100b55ddc713b4a4c3d127a29f88495",
+    "content": [
+      {
+        "type": "text",
+        "text": "你好关涛,我在执行小红书搜索摄影主题的任务时遇到了问题。虽然我已经加载了cookie,但是在搜索\"摄影\"后,页面仍然弹出登录窗口要求登录才能查看搜索结果。请问有什么解决方案吗?"
+      }
+    ]
+  },
+  {
+    "role": "assistant",
+    "message_id": "om_x100b55ddc563b8a4c4cef0e822739bd",
+    "content": [
+      {
+        "type": "text",
+        "text": "好的,这是浏览器的live url:https://live.browser-use.com?wss=https%3A//5c550871-7d57-4c04-a4fe-585fd7a346db.cdp5.browser-use.com\n\n你可以通过这个链接查看和操作浏览器。"
+      }
+    ]
+  },
+  {
+    "role": "assistant",
+    "message_id": "om_x100b55dded5da8a0c35c56f0dd0d78a",
+    "content": [
+      {
+        "type": "text",
+        "text": "你好,我需要访问小红书,但需要扫码登录。请帮我在以下链接中完成登录操作:\n\n"
+      },
+      {
+        "type": "text",
+        "text": "https://live.browser-use.com?wss=https%3A//fc0dfb3f-7854-41ef-8579-3aa3d86e3b02.cdp4.browser-use.com"
+      },
+      {
+        "type": "text",
+        "text": "\n\n登录完成后请回复我,谢谢!"
+      }
+    ]
+  }
+]

+ 12 - 0
agent/tools/builtin/feishu/chat_history/chat_孙若天.json

@@ -0,0 +1,12 @@
+[
+  {
+    "role": "assistant",
+    "message_id": "om_x100b55ddfb31b898c3a9ee232316f13",
+    "content": [
+      {
+        "type": "text",
+        "text": "你好,我需要访问小红书,但是需要登录。请帮我在这个浏览器中完成登录:\n\nhttps://live.browser-use.com?wss=https%3A//9dcf552b-18ea-4f7f-9ace-96ca6fc9a3ac.cdp3.browser-use.com\n\n登录完成后请回复我一下,谢谢!"
+      }
+    ]
+  }
+]

+ 12 - 0
agent/tools/builtin/feishu/chat_history/chat_谭景玉.json

@@ -0,0 +1,12 @@
+[
+  {
+    "role": "assistant",
+    "message_id": "om_x100b55dca36b98acc14eba0a6709aa0",
+    "content": [
+      {
+        "type": "text",
+        "text": "你好!我需要登录小红书来完成搜索摄影主题的任务,但是没有找到保存的cookie。\n\n请点击以下链接在浏览器中完成小红书登录:\nhttps://live.browser-use.com?wss=wss%3A//4599a061-1830-4cb0-99fc-fffb5503e99a.cdp1.browser-use.com/devtools/browser/f77323a4-3759-4558-85e0-f4eb3eb04368\n\n登录完成后请告诉我,我会保存登录状态。谢谢!"
+      }
+    ]
+  }
+]

+ 1 - 1
agent/tools/builtin/file/edit.py

@@ -17,7 +17,7 @@ import re
 from agent.tools import tool, ToolResult, ToolContext
 from agent.tools import tool, ToolResult, ToolContext
 
 
 
 
-@tool(description="编辑文件,使用精确字符串替换。支持多种智能匹配策略。")
+@tool(description="编辑文件,使用精确字符串替换。支持多种智能匹配策略。", hidden_params=["context"])
 async def edit_file(
 async def edit_file(
     file_path: str,
     file_path: str,
     old_string: str,
     old_string: str,

+ 1 - 1
agent/tools/builtin/file/glob.py

@@ -19,7 +19,7 @@ from agent.tools import tool, ToolResult, ToolContext
 LIMIT = 100  # 最大返回数量(参考 opencode glob.ts:35)
 LIMIT = 100  # 最大返回数量(参考 opencode glob.ts:35)
 
 
 
 
-@tool(description="使用 glob 模式匹配文件")
+@tool(description="使用 glob 模式匹配文件", hidden_params=["context"])
 async def glob_files(
 async def glob_files(
     pattern: str,
     pattern: str,
     path: Optional[str] = None,
     path: Optional[str] = None,

+ 1 - 1
agent/tools/builtin/file/grep.py

@@ -21,7 +21,7 @@ LIMIT = 100  # 最大返回匹配数(参考 opencode grep.ts:107)
 MAX_LINE_LENGTH = 2000  # 最大行长度(参考 opencode grep.ts:10)
 MAX_LINE_LENGTH = 2000  # 最大行长度(参考 opencode grep.ts:10)
 
 
 
 
-@tool(description="在文件内容中搜索模式")
+@tool(description="在文件内容中搜索模式", hidden_params=["context"])
 async def grep_content(
 async def grep_content(
     pattern: str,
     pattern: str,
     path: Optional[str] = None,
     path: Optional[str] = None,

+ 1 - 1
agent/tools/builtin/file/read.py

@@ -27,7 +27,7 @@ MAX_LINE_LENGTH = 2000
 MAX_BYTES = 50 * 1024  # 50KB
 MAX_BYTES = 50 * 1024  # 50KB
 
 
 
 
-@tool(description="读取文件内容,支持文本文件、图片、PDF 等多种格式,也支持 HTTP/HTTPS URL")
+@tool(description="读取文件内容,支持文本文件、图片、PDF 等多种格式,也支持 HTTP/HTTPS URL", hidden_params=["context"])
 async def read_file(
 async def read_file(
     file_path: str,
     file_path: str,
     offset: int = 0,
     offset: int = 0,

+ 1 - 1
agent/tools/builtin/file/write.py

@@ -16,7 +16,7 @@ import difflib
 from agent.tools import tool, ToolResult, ToolContext
 from agent.tools import tool, ToolResult, ToolContext
 
 
 
 
-@tool(description="写入文件内容(创建新文件、覆盖现有文件或追加内容)")
+@tool(description="写入文件内容(创建新文件、覆盖现有文件或追加内容)", hidden_params=["context"])
 async def write_file(
 async def write_file(
     file_path: str,
     file_path: str,
     content: str,
     content: str,

+ 1 - 1
agent/tools/builtin/glob_tool.py

@@ -19,7 +19,7 @@ from agent.tools import tool, ToolResult, ToolContext
 LIMIT = 100  # 最大返回数量(参考 opencode glob.ts:35)
 LIMIT = 100  # 最大返回数量(参考 opencode glob.ts:35)
 
 
 
 
-@tool(description="使用 glob 模式匹配文件")
+@tool(description="使用 glob 模式匹配文件", hidden_params=["context"])
 async def glob_files(
 async def glob_files(
     pattern: str,
     pattern: str,
     path: Optional[str] = None,
     path: Optional[str] = None,

+ 611 - 0
agent/tools/builtin/knowledge.py

@@ -0,0 +1,611 @@
+"""
+知识管理工具 - KnowHub API 封装
+
+所有工具通过 HTTP API 调用 KnowHub Server。
+"""
+
+import os
+import logging
+import subprocess
+import httpx
+from dataclasses import dataclass
+from typing import List, Dict, Optional, Any
+from agent.tools import tool, ToolResult, ToolContext
+from agent.core.prompts import build_reflect_prompt, COMPLETION_REFLECT_PROMPT
+
+logger = logging.getLogger(__name__)
+
+# KnowHub Server API 地址(去除末尾斜杠)
+KNOWHUB_API = os.getenv("KNOWHUB_API", "http://localhost:8000").rstrip("/")
+
+
+# ===== 知识管理配置 =====
+
+@dataclass
+class KnowledgeConfig:
+    """知识提取与注入的配置"""
+
+    # 压缩时提取(消息量超阈值触发压缩时,在 Level 1 过滤前用完整 history 反思)
+    enable_extraction: bool = True         # 是否在压缩触发时提取知识
+    reflect_prompt: str = ""               # 自定义反思 prompt;空则使用默认,见 agent/core/prompts/knowledge.py:REFLECT_PROMPT
+
+    # agent运行完成后提取(不代表任务完成,agent 可能中途退出等待人工评估)
+    enable_completion_extraction: bool = True      # 是否在运行完成后提取知识
+    completion_reflect_prompt: str = ""            # 自定义复盘 prompt;空则使用默认,见 agent/core/prompts/knowledge.py:COMPLETION_REFLECT_PROMPT
+
+    # 知识注入(agent切换当前工作的goal时,自动注入相关知识)
+    enable_injection: bool = True          # 是否在 focus goal 时自动注入相关知识
+
+    # 默认字段(保存/搜索时自动注入)
+    owner: str = ""                            # 所有者(空则尝试从 git config user.email 获取,再空则用 agent:{agent_id})
+    default_tags: Optional[Dict[str, str]] = None      # 默认 tags(会与工具调用参数合并)
+    default_scopes: Optional[List[str]] = None         # 默认 scopes(空则用 ["org:cybertogether"])
+    default_search_types: Optional[List[str]] = None   # 默认搜索类型过滤
+    default_search_owner: str = ""                     # 默认搜索 owner 过滤(空则不过滤)
+
+    def get_reflect_prompt(self) -> str:
+        """压缩时反思 prompt"""
+        return self.reflect_prompt if self.reflect_prompt else build_reflect_prompt()
+
+    def get_completion_reflect_prompt(self) -> str:
+        """任务完成后复盘 prompt"""
+        return self.completion_reflect_prompt if self.completion_reflect_prompt else COMPLETION_REFLECT_PROMPT
+
+    @property
+    def resolved_owner(self) -> str:
+        """解析后的 owner(优先级:配置 > git email > 'agent')
+
+        供 inject_params key path 使用:knowledge_config.resolved_owner
+        """
+        if self.owner:
+            return self.owner
+
+        # 尝试从 git config 获取
+        try:
+            result = subprocess.run(
+                ["git", "config", "user.email"],
+                capture_output=True,
+                text=True,
+                timeout=2,
+            )
+            if result.returncode == 0 and result.stdout.strip():
+                return result.stdout.strip()
+        except Exception:
+            pass
+
+        return "agent"
+
+    def get_owner(self, agent_id: str = "agent") -> str:
+        """获取 owner(优先级:配置 > git email > agent:{agent_id})"""
+        owner = self.resolved_owner
+        if owner == "agent" and agent_id != "agent":
+            return f"agent:{agent_id}"
+        return owner
+
+
+@tool(
+    hidden_params=["context"],
+    inject_params={
+        "types": {"mode": "default", "key": "knowledge_config.default_search_types"},
+        "owner": {"mode": "default", "key": "knowledge_config.default_search_owner"},
+    }
+)
+async def knowledge_search(
+    query: str,
+    top_k: int = 5,
+    min_score: int = 3,
+    types: Optional[List[str]] = None,
+    owner: Optional[str] = None,
+    context: Optional[ToolContext] = None,
+) -> ToolResult:
+    """
+    检索知识(两阶段:语义路由 + 质量精排)
+
+    Args:
+        query: 搜索查询(任务描述)
+        top_k: 返回数量(默认 5)
+        min_score: 最低评分过滤(默认 3)
+        types: 按类型过滤(user_profile/strategy/tool/usecase/definition/plan)
+        owner: 按所有者过滤(可选)
+        context: 工具上下文
+
+    Returns:
+        相关知识列表
+    """
+    try:
+        params = {
+            "q": query,
+            "top_k": top_k,
+            "min_score": min_score,
+        }
+        if types:
+            params["types"] = ",".join(types)
+        if owner:
+            params["owner"] = owner
+
+        async with httpx.AsyncClient(timeout=60.0) as client:
+            response = await client.get(f"{KNOWHUB_API}/api/knowledge/search", params=params)
+            response.raise_for_status()
+            data = response.json()
+
+        results = data.get("results", [])
+        count = data.get("count", 0)
+
+        if not results:
+            return ToolResult(
+                title="🔍 未找到相关知识",
+                output=f"查询: {query}\n\n知识库中暂无相关的高质量知识。",
+                long_term_memory=f"知识检索: 未找到相关知识 - {query[:50]}"
+            )
+
+        # 格式化输出
+        output_lines = [f"查询: {query}\n", f"找到 {count} 条相关知识:\n"]
+
+        for idx, item in enumerate(results, 1):
+            eval_data = item.get("eval", {})
+            score = eval_data.get("score", 3)
+            output_lines.append(f"\n### {idx}. [{item['id']}] (⭐ {score})")
+            output_lines.append(f"**任务**: {item['task'][:150]}...")
+            output_lines.append(f"**内容**: {item['content'][:200]}...")
+
+        return ToolResult(
+            title="✅ 知识检索成功",
+            output="\n".join(output_lines),
+            long_term_memory=f"知识检索: 找到 {count} 条相关知识 - {query[:50]}",
+            metadata={
+                "count": count,
+                "knowledge_ids": [item["id"] for item in results],
+                "items": results
+            }
+        )
+
+    except Exception as e:
+        logger.error(f"知识检索失败: {e}")
+        return ToolResult(
+            title="❌ 检索失败",
+            output=f"错误: {str(e)}",
+            error=str(e)
+        )
+
+
+@tool(
+    hidden_params=["context", "owner"],
+    inject_params={
+        "owner": {"mode": "default", "key": "knowledge_config.resolved_owner"},
+        "tags": {"mode": "merge", "key": "knowledge_config.default_tags"},
+        "scopes": {"mode": "merge", "key": "knowledge_config.default_scopes"},
+    }
+)
+async def knowledge_save(
+    task: str,
+    content: str,
+    types: List[str],
+    tags: Optional[Dict[str, str]] = None,
+    scopes: Optional[List[str]] = None,
+    owner: Optional[str] = None,
+    resource_ids: Optional[List[str]] = None,
+    source_name: str = "",
+    source_category: str = "exp",
+    urls: List[str] = None,
+    agent_id: str = "research_agent",
+    submitted_by: str = "",
+    score: int = 3,
+    message_id: str = "",
+    context: Optional[ToolContext] = None,
+) -> ToolResult:
+    """
+    保存新知识
+
+    Args:
+        task: 任务描述(在什么情景下 + 要完成什么目标)
+        content: 核心内容
+        types: 知识类型标签,可选:user_profile, strategy, tool, usecase, definition, plan
+        tags: 业务标签(JSON 对象)
+        scopes: 可见范围(默认 ["org:cybertogether"])
+        owner: 所有者(默认 agent:{agent_id})
+        resource_ids: 关联的资源 ID 列表(可选)
+        source_name: 来源名称
+        source_category: 来源类别(paper/exp/skill/book)
+        urls: 参考来源链接列表
+        agent_id: 执行此调研的 agent ID
+        submitted_by: 提交者
+        score: 初始评分 1-5(默认 3)
+        message_id: 来源 Message ID
+        context: 工具上下文
+
+    Returns:
+        保存结果
+    """
+    try:
+        # 设置默认值(在 agent 代码中,不是服务器端)
+        if scopes is None:
+            scopes = ["org:cybertogether"]
+        if owner is None:
+            owner = f"agent:{agent_id}"
+
+        payload = {
+            "message_id": message_id,
+            "types": types,
+            "task": task,
+            "tags": tags or {},
+            "scopes": scopes,
+            "owner": owner,
+            "content": content,
+            "resource_ids": resource_ids or [],
+            "source": {
+                "name": source_name,
+                "category": source_category,
+                "urls": urls or [],
+                "agent_id": agent_id,
+                "submitted_by": submitted_by,
+            },
+            "eval": {
+                "score": score,
+                "helpful": 1,
+                "harmful": 0,
+                "confidence": 0.5,
+            }
+        }
+
+        async with httpx.AsyncClient(timeout=30.0) as client:
+            response = await client.post(f"{KNOWHUB_API}/api/knowledge", json=payload)
+            response.raise_for_status()
+            data = response.json()
+
+        knowledge_id = data.get("knowledge_id", "unknown")
+
+        return ToolResult(
+            title="✅ 知识已保存",
+            output=f"知识 ID: {knowledge_id}\n\n任务:\n{task[:100]}...",
+            long_term_memory=f"保存知识: {knowledge_id} - {task[:50]}",
+            metadata={"knowledge_id": knowledge_id}
+        )
+
+    except Exception as e:
+        logger.error(f"保存知识失败: {e}")
+        return ToolResult(
+            title="❌ 保存失败",
+            output=f"错误: {str(e)}",
+            error=str(e)
+        )
+
+
+@tool(hidden_params=["context"])
+async def knowledge_update(
+    knowledge_id: str,
+    add_helpful_case: Optional[Dict] = None,
+    add_harmful_case: Optional[Dict] = None,
+    update_score: Optional[int] = None,
+    evolve_feedback: Optional[str] = None,
+    context: Optional[ToolContext] = None,
+) -> ToolResult:
+    """
+    更新已有知识的评估反馈
+
+    Args:
+        knowledge_id: 知识 ID
+        add_helpful_case: 添加好用的案例
+        add_harmful_case: 添加不好用的案例
+        update_score: 更新评分(1-5)
+        evolve_feedback: 经验进化反馈(触发 LLM 重写)
+        context: 工具上下文
+
+    Returns:
+        更新结果
+    """
+    try:
+        payload = {}
+        if add_helpful_case:
+            payload["add_helpful_case"] = add_helpful_case
+        if add_harmful_case:
+            payload["add_harmful_case"] = add_harmful_case
+        if update_score is not None:
+            payload["update_score"] = update_score
+        if evolve_feedback:
+            payload["evolve_feedback"] = evolve_feedback
+
+        if not payload:
+            return ToolResult(
+                title="⚠️ 无更新",
+                output="未指定任何更新内容",
+                long_term_memory="尝试更新知识但未指定更新内容"
+            )
+
+        async with httpx.AsyncClient(timeout=60.0) as client:
+            response = await client.put(f"{KNOWHUB_API}/api/knowledge/{knowledge_id}", json=payload)
+            response.raise_for_status()
+
+        summary = []
+        if add_helpful_case:
+            summary.append("添加 helpful 案例")
+        if add_harmful_case:
+            summary.append("添加 harmful 案例")
+        if update_score is not None:
+            summary.append(f"更新评分: {update_score}")
+        if evolve_feedback:
+            summary.append("知识进化: 基于反馈重写内容")
+
+        return ToolResult(
+            title="✅ 知识已更新",
+            output=f"知识 ID: {knowledge_id}\n\n更新内容:\n" + "\n".join(f"- {s}" for s in summary),
+            long_term_memory=f"更新知识: {knowledge_id}"
+        )
+
+    except Exception as e:
+        logger.error(f"更新知识失败: {e}")
+        return ToolResult(
+            title="❌ 更新失败",
+            output=f"错误: {str(e)}",
+            error=str(e)
+        )
+
+
+@tool(hidden_params=["context"])
+async def knowledge_batch_update(
+    feedback_list: List[Dict[str, Any]],
+    context: Optional[ToolContext] = None,
+) -> ToolResult:
+    """
+    批量反馈知识的有效性
+
+    Args:
+        feedback_list: 评价列表,每个元素包含:
+            - knowledge_id: (str) 知识 ID
+            - is_effective: (bool) 是否有效
+            - feedback: (str, optional) 改进建议,若有效且有建议则触发知识进化
+        context: 工具上下文
+
+    Returns:
+        批量更新结果
+    """
+    try:
+        if not feedback_list:
+            return ToolResult(
+                title="⚠️ 反馈列表为空",
+                output="未提供任何反馈",
+                long_term_memory="批量更新知识: 反馈列表为空"
+            )
+
+        payload = {"feedback_list": feedback_list}
+
+        async with httpx.AsyncClient(timeout=120.0) as client:
+            response = await client.post(f"{KNOWHUB_API}/api/knowledge/batch_update", json=payload)
+            response.raise_for_status()
+            data = response.json()
+
+        updated = data.get("updated", 0)
+
+        return ToolResult(
+            title="✅ 批量更新完成",
+            output=f"成功更新 {updated} 条知识",
+            long_term_memory=f"批量更新知识: 成功 {updated} 条"
+        )
+
+    except Exception as e:
+        logger.error(f"列出知识失败: {e}")
+        return ToolResult(
+            title="❌ 列表失败",
+            output=f"错误: {str(e)}",
+            error=str(e)
+        )
+
+
+@tool(hidden_params=["context"])
+async def knowledge_list(
+    limit: int = 10,
+    types: Optional[List[str]] = None,
+    scopes: Optional[List[str]] = None,
+    context: Optional[ToolContext] = None,
+) -> ToolResult:
+    """
+    列出已保存的知识
+
+    Args:
+        limit: 返回数量限制(默认 10)
+        types: 按类型过滤(可选)
+        scopes: 按范围过滤(可选)
+        context: 工具上下文
+
+    Returns:
+        知识列表
+    """
+    try:
+        params = {"limit": limit}
+        if types:
+            params["types"] = ",".join(types)
+        if scopes:
+            params["scopes"] = ",".join(scopes)
+
+        async with httpx.AsyncClient(timeout=30.0) as client:
+            response = await client.get(f"{KNOWHUB_API}/api/knowledge", params=params)
+            response.raise_for_status()
+            data = response.json()
+
+        results = data.get("results", [])
+        count = data.get("count", 0)
+
+        if not results:
+            return ToolResult(
+                title="📂 知识库为空",
+                output="还没有保存任何知识",
+                long_term_memory="知识库为空"
+            )
+
+        output_lines = [f"共找到 {count} 条知识:\n"]
+        for item in results:
+            eval_data = item.get("eval", {})
+            score = eval_data.get("score", 3)
+            output_lines.append(f"- [{item['id']}] (⭐{score}) {item['task'][:60]}...")
+
+        return ToolResult(
+            title="📚 知识列表",
+            output="\n".join(output_lines),
+            long_term_memory=f"列出 {count} 条知识"
+        )
+
+    except Exception as e:
+        logger.error(f"列出知识失败: {e}")
+        return ToolResult(
+            title="❌ 列表失败",
+            output=f"错误: {str(e)}",
+            error=str(e)
+        )
+
+
+@tool(hidden_params=["context"])
+async def knowledge_slim(
+    model: str = "google/gemini-2.0-flash-001",
+    context: Optional[ToolContext] = None,
+) -> ToolResult:
+    """
+    知识库瘦身:调用顶级大模型,将知识库中语义相似的知识合并精简
+
+    Args:
+        model: 使用的模型(默认 gemini-2.0-flash-001)
+        context: 工具上下文
+
+    Returns:
+        瘦身结果报告
+    """
+    try:
+        async with httpx.AsyncClient(timeout=300.0) as client:
+            response = await client.post(f"{KNOWHUB_API}/api/knowledge/slim", params={"model": model})
+            response.raise_for_status()
+            data = response.json()
+
+        before = data.get("before", 0)
+        after = data.get("after", 0)
+        report = data.get("report", "")
+
+        result = f"瘦身完成:{before} → {after} 条知识"
+        if report:
+            result += f"\n{report}"
+
+        return ToolResult(
+            title="✅ 知识库瘦身完成",
+            output=result,
+            long_term_memory=f"知识库瘦身: {before} → {after} 条"
+        )
+
+    except Exception as e:
+        logger.error(f"知识库瘦身失败: {e}")
+        return ToolResult(
+            title="❌ 瘦身失败",
+            output=f"错误: {str(e)}",
+            error=str(e)
+        )
+
+
+# ==================== Resource 资源管理工具 ====================
+
+@tool(hidden_params=["context"])
+async def resource_save(
+    resource_id: str,
+    title: str,
+    body: str,
+    content_type: str = "text",
+    secure_body: str = "",
+    metadata: Optional[Dict[str, Any]] = None,
+    submitted_by: str = "",
+    context: Optional[ToolContext] = None,
+) -> ToolResult:
+    """
+    保存资源(代码片段、凭证、Cookie 等)
+
+    Args:
+        resource_id: 资源 ID(层级路径,如 "code/selenium/login" 或 "credentials/website_a")
+        title: 资源标题
+        body: 公开内容(明文存储,可搜索)
+        content_type: 内容类型(text/code/credential/cookie)
+        secure_body: 敏感内容(加密存储,需要组织密钥访问)
+        metadata: 元数据(如 {"language": "python", "acquired_at": "2026-03-06T10:00:00Z"})
+        submitted_by: 提交者
+        context: 工具上下文
+
+    Returns:
+        保存结果
+    """
+    try:
+        payload = {
+            "id": resource_id,
+            "title": title,
+            "body": body,
+            "secure_body": secure_body,
+            "content_type": content_type,
+            "metadata": metadata or {},
+            "submitted_by": submitted_by,
+        }
+
+        async with httpx.AsyncClient(timeout=30.0) as client:
+            response = await client.post(f"{KNOWHUB_API}/api/resource", json=payload)
+            response.raise_for_status()
+            data = response.json()
+
+        return ToolResult(
+            title="✅ 资源已保存",
+            output=f"资源 ID: {resource_id}\n类型: {content_type}\n标题: {title}",
+            long_term_memory=f"保存资源: {resource_id} ({content_type})",
+            metadata={"resource_id": resource_id}
+        )
+
+    except Exception as e:
+        logger.error(f"保存资源失败: {e}")
+        return ToolResult(
+            title="❌ 保存失败",
+            output=f"错误: {str(e)}",
+            error=str(e)
+        )
+
+
+@tool(hidden_params=["context"])
+async def resource_get(
+    resource_id: str,
+    org_key: Optional[str] = None,
+    context: Optional[ToolContext] = None,
+) -> ToolResult:
+    """
+    获取资源内容
+
+    Args:
+        resource_id: 资源 ID(层级路径)
+        org_key: 组织密钥(用于解密敏感内容,可选)
+        context: 工具上下文
+
+    Returns:
+        资源内容
+    """
+    try:
+        headers = {}
+        if org_key:
+            headers["X-Org-Key"] = org_key
+
+        async with httpx.AsyncClient(timeout=30.0) as client:
+            response = await client.get(
+                f"{KNOWHUB_API}/api/resource/{resource_id}",
+                headers=headers
+            )
+            response.raise_for_status()
+            data = response.json()
+
+        output = f"资源 ID: {data['id']}\n"
+        output += f"标题: {data['title']}\n"
+        output += f"类型: {data['content_type']}\n"
+        output += f"\n公开内容:\n{data['body']}\n"
+
+        if data.get('secure_body'):
+            output += f"\n敏感内容:\n{data['secure_body']}\n"
+
+        return ToolResult(
+            title=f"📦 {data['title']}",
+            output=output,
+            metadata=data
+        )
+
+    except Exception as e:
+        logger.error(f"获取资源失败: {e}")
+        return ToolResult(
+            title="❌ 获取失败",
+            output=f"错误: {str(e)}",
+            error=str(e)
+        )
+

+ 3 - 0
agent/tools/builtin/sandbox.py

@@ -19,6 +19,7 @@ DEFAULT_TIMEOUT = 300.0
 
 
 
 
 @tool(
 @tool(
+    hidden_params=["context"],
     display={
     display={
         "zh": {
         "zh": {
             "name": "创建沙盒环境",
             "name": "创建沙盒环境",
@@ -120,6 +121,7 @@ async def sandbox_create_environment(
 
 
 
 
 @tool(
 @tool(
+    hidden_params=["context"],
     display={
     display={
         "zh": {
         "zh": {
             "name": "执行沙盒命令",
             "name": "执行沙盒命令",
@@ -234,6 +236,7 @@ async def sandbox_run_shell(
 
 
 
 
 @tool(
 @tool(
+    hidden_params=["context"],
     display={
     display={
         "zh": {
         "zh": {
             "name": "重建沙盒端口",
             "name": "重建沙盒端口",

+ 3 - 3
agent/tools/builtin/skill.py

@@ -10,12 +10,12 @@ from pathlib import Path
 from typing import Optional
 from typing import Optional
 
 
 from agent.tools import tool, ToolResult
 from agent.tools import tool, ToolResult
-from agent.memory.skill_loader import SkillLoader
+from agent.skill.skill_loader import SkillLoader
 
 
 # 默认 skills 目录(优先级:项目 skills > 框架 skills)
 # 默认 skills 目录(优先级:项目 skills > 框架 skills)
 DEFAULT_SKILLS_DIRS = [
 DEFAULT_SKILLS_DIRS = [
     os.getenv("SKILLS_DIR", "./skills"),      # 项目特定 skills(优先)
     os.getenv("SKILLS_DIR", "./skills"),      # 项目特定 skills(优先)
-    "./agent/memory/skills"                    # 框架内置 skills
+    "./agent/skill/skills"                    # 框架内置 skills
 ]
 ]
 
 
 # 默认单一目录(用于 list_skills)
 # 默认单一目录(用于 list_skills)
@@ -36,7 +36,7 @@ def _check_skill_setup(skill_name: str) -> Optional[str]:
     if skill_name in ["browser-use", "browser_use"]:
     if skill_name in ["browser-use", "browser_use"]:
         try:
         try:
             # 动态导入 browser-use skill 的 setup 模块
             # 动态导入 browser-use skill 的 setup 模块
-            from agent.memory.skills.browser_use.setup import (
+            from agent.skill.skills.browser_use.setup import (
                 _check_browser_use_cli,
                 _check_browser_use_cli,
                 _check_chromium_installed
                 _check_chromium_installed
             )
             )

+ 78 - 36
agent/tools/builtin/subagent.py

@@ -16,6 +16,62 @@ from agent.trace.goal_models import GoalTree
 from agent.trace.websocket import broadcast_sub_trace_started, broadcast_sub_trace_completed
 from agent.trace.websocket import broadcast_sub_trace_started, broadcast_sub_trace_completed
 
 
 
 
+# ===== prompts =====
+
+# ===== 评估任务 =====
+
+EVALUATE_PROMPT_TEMPLATE = """# 评估任务
+
+请评估以下任务的执行结果是否满足要求。
+
+## 目标描述
+
+{goal_description}
+
+## 执行结果
+
+{result_text}
+
+## 输出格式
+
+## 评估结论
+[通过/不通过]
+
+## 评估理由
+[详细说明通过或不通过原因]
+
+## 修改建议(如果不通过)
+1. [建议1]
+2. [建议2]
+"""
+
+# ===== 结果格式化 =====
+
+DELEGATE_RESULT_HEADER = "## 委托任务完成\n"
+
+DELEGATE_SAVED_KNOWLEDGE_HEADER = "**保存的知识** ({count} 条):"
+
+DELEGATE_STATS_HEADER = "**执行统计**:"
+
+EXPLORE_RESULT_HEADER = "## 探索结果\n"
+
+EXPLORE_BRANCH_TEMPLATE = "### 方案 {branch_name}: {task}"
+
+EXPLORE_STATUS_SUCCESS = "**状态**: ✓ 完成"
+
+EXPLORE_STATUS_FAILED = "**状态**: ✗ 失败"
+
+EXPLORE_STATUS_ERROR = "**状态**: ✗ 异常"
+
+EXPLORE_SUMMARY_HEADER = "## 总结"
+
+def build_evaluate_prompt(goal_description: str, result_text: str) -> str:
+    return EVALUATE_PROMPT_TEMPLATE.format(
+        goal_description=goal_description,
+        result_text=result_text or "(无执行结果)",
+    )
+
+
 def _make_run_config(**kwargs):
 def _make_run_config(**kwargs):
     """延迟导入 RunConfig 以避免循环导入"""
     """延迟导入 RunConfig 以避免循环导入"""
     from agent.core.runner import RunConfig
     from agent.core.runner import RunConfig
@@ -128,13 +184,23 @@ def _get_allowed_tools(single: bool, context: dict) -> Optional[List[str]]:
 
 
 def _format_single_result(result: Dict[str, Any], sub_trace_id: str, continued: bool) -> Dict[str, Any]:
 def _format_single_result(result: Dict[str, Any], sub_trace_id: str, continued: bool) -> Dict[str, Any]:
     """格式化单任务(delegate)结果"""
     """格式化单任务(delegate)结果"""
-    lines = ["## 委托任务完成\n"]
+    lines = [DELEGATE_RESULT_HEADER]
     summary = result.get("summary", "")
     summary = result.get("summary", "")
     if summary:
     if summary:
         lines.append(summary)
         lines.append(summary)
         lines.append("")
         lines.append("")
+
+    # 添加保存的知识 ID
+    saved_knowledge_ids = result.get("saved_knowledge_ids", [])
+    if saved_knowledge_ids:
+        lines.append("---\n")
+        lines.append(DELEGATE_SAVED_KNOWLEDGE_HEADER.format(count=len(saved_knowledge_ids)))
+        for kid in saved_knowledge_ids:
+            lines.append(f"- {kid}")
+        lines.append("")
+
     lines.append("---\n")
     lines.append("---\n")
-    lines.append("**执行统计**:")
+    lines.append(DELEGATE_STATS_HEADER)
     stats = result.get("stats", {})
     stats = result.get("stats", {})
     if stats:
     if stats:
         lines.append(f"- 消息数: {stats.get('total_messages', 0)}")
         lines.append(f"- 消息数: {stats.get('total_messages', 0)}")
@@ -146,6 +212,7 @@ def _format_single_result(result: Dict[str, Any], sub_trace_id: str, continued:
         "mode": "delegate",
         "mode": "delegate",
         "sub_trace_id": sub_trace_id,
         "sub_trace_id": sub_trace_id,
         "continue_from": continued,
         "continue_from": continued,
+        "saved_knowledge_ids": saved_knowledge_ids,  # 传递给父 agent
         **result,
         **result,
         "summary": formatted_summary,
         "summary": formatted_summary,
     }
     }
@@ -155,7 +222,7 @@ def _format_multi_result(
     tasks: List[str], results: List[Dict[str, Any]], sub_trace_ids: List[Dict]
     tasks: List[str], results: List[Dict[str, Any]], sub_trace_ids: List[Dict]
 ) -> Dict[str, Any]:
 ) -> Dict[str, Any]:
     """格式化多任务(explore)聚合结果"""
     """格式化多任务(explore)聚合结果"""
-    lines = ["## 探索结果\n"]
+    lines = [EXPLORE_RESULT_HEADER]
     successful = 0
     successful = 0
     failed = 0
     failed = 0
     total_tokens = 0
     total_tokens = 0
@@ -163,15 +230,15 @@ def _format_multi_result(
 
 
     for i, (task_item, result) in enumerate(zip(tasks, results)):
     for i, (task_item, result) in enumerate(zip(tasks, results)):
         branch_name = chr(ord('A') + i)
         branch_name = chr(ord('A') + i)
-        lines.append(f"### 方案 {branch_name}: {task_item}")
+        lines.append(EXPLORE_BRANCH_TEMPLATE.format(branch_name=branch_name, task=task_item))
 
 
         if isinstance(result, dict):
         if isinstance(result, dict):
             status = result.get("status", "unknown")
             status = result.get("status", "unknown")
             if status == "completed":
             if status == "completed":
-                lines.append("**状态**: ✓ 完成")
+                lines.append(EXPLORE_STATUS_SUCCESS)
                 successful += 1
                 successful += 1
             else:
             else:
-                lines.append("**状态**: ✗ 失败")
+                lines.append(EXPLORE_STATUS_FAILED)
                 failed += 1
                 failed += 1
 
 
             summary = result.get("summary", "")
             summary = result.get("summary", "")
@@ -187,13 +254,13 @@ def _format_multi_result(
                 total_tokens += tokens
                 total_tokens += tokens
                 total_cost += cost
                 total_cost += cost
         else:
         else:
-            lines.append("**状态**: ✗ 异常")
+            lines.append(EXPLORE_STATUS_ERROR)
             failed += 1
             failed += 1
 
 
         lines.append("")
         lines.append("")
 
 
     lines.append("---\n")
     lines.append("---\n")
-    lines.append("## 总结")
+    lines.append(EXPLORE_SUMMARY_HEADER)
     lines.append(f"- 总分支数: {len(tasks)}")
     lines.append(f"- 总分支数: {len(tasks)}")
     lines.append(f"- 成功: {successful}")
     lines.append(f"- 成功: {successful}")
     lines.append(f"- 失败: {failed}")
     lines.append(f"- 失败: {failed}")
@@ -248,32 +315,7 @@ def _build_evaluate_prompt(goal_description: str, messages: Optional[Messages])
                         parts.append(item.get("text", ""))
                         parts.append(item.get("text", ""))
         result_text = "\n".join(parts)
         result_text = "\n".join(parts)
 
 
-    lines = [
-        "# 评估任务",
-        "",
-        "请评估以下任务的执行结果是否满足要求。",
-        "",
-        "## 目标描述",
-        "",
-        goal_description,
-        "",
-        "## 执行结果",
-        "",
-        result_text or "(无执行结果)",
-        "",
-        "## 输出格式",
-        "",
-        "## 评估结论",
-        "[通过/不通过]",
-        "",
-        "## 评估理由",
-        "[详细说明通过或不通过原因]",
-        "",
-        "## 修改建议(如果不通过)",
-        "1. [建议1]",
-        "2. [建议2]",
-    ]
-    return "\n".join(lines)
+    return build_evaluate_prompt(goal_description, result_text)
 
 
 
 
 def _make_event_printer(label: str):
 def _make_event_printer(label: str):
@@ -537,7 +579,7 @@ async def _run_agents(
 
 
 # ===== 工具定义 =====
 # ===== 工具定义 =====
 
 
-@tool(description="创建 Agent 执行任务")
+@tool(description="创建 Agent 执行任务", hidden_params=["context"])
 async def agent(
 async def agent(
     task: Union[str, List[str]],
     task: Union[str, List[str]],
     messages: Optional[Union[Messages, List[Messages]]] = None,
     messages: Optional[Union[Messages, List[Messages]]] = None,
@@ -604,7 +646,7 @@ async def agent(
     )
     )
 
 
 
 
-@tool(description="评估目标执行结果是否满足要求")
+@tool(description="评估目标执行结果是否满足要求", hidden_params=["context"])
 async def evaluate(
 async def evaluate(
     messages: Optional[Messages] = None,
     messages: Optional[Messages] = None,
     target_goal_id: Optional[str] = None,
     target_goal_id: Optional[str] = None,

+ 94 - 11
agent/tools/registry.py

@@ -66,7 +66,9 @@ class ToolRegistry:
 		requires_confirmation: bool = False,
 		requires_confirmation: bool = False,
 		editable_params: Optional[List[str]] = None,
 		editable_params: Optional[List[str]] = None,
 		display: Optional[Dict[str, Dict[str, Any]]] = None,
 		display: Optional[Dict[str, Dict[str, Any]]] = None,
-		url_patterns: Optional[List[str]] = None
+		url_patterns: Optional[List[str]] = None,
+		hidden_params: Optional[List[str]] = None,
+		inject_params: Optional[Dict[str, Any]] = None
 	):
 	):
 		"""
 		"""
 		注册工具
 		注册工具
@@ -78,6 +80,8 @@ class ToolRegistry:
 			editable_params: 允许用户编辑的参数列表
 			editable_params: 允许用户编辑的参数列表
 			display: i18n 展示信息 {"zh": {"name": "xx", "params": {...}}, "en": {...}}
 			display: i18n 展示信息 {"zh": {"name": "xx", "params": {...}}, "en": {...}}
 			url_patterns: URL 模式列表(如 ["*.google.com"],None = 无限制)
 			url_patterns: URL 模式列表(如 ["*.google.com"],None = 无限制)
+			hidden_params: 隐藏参数列表(不生成 schema,LLM 看不到)
+			inject_params: 注入参数规则 {param_name: injector_func}
 		"""
 		"""
 		func_name = func.__name__
 		func_name = func.__name__
 
 
@@ -85,7 +89,7 @@ class ToolRegistry:
 		if schema is None:
 		if schema is None:
 			try:
 			try:
 				from agent.tools.schema import SchemaGenerator
 				from agent.tools.schema import SchemaGenerator
-				schema = SchemaGenerator.generate(func)
+				schema = SchemaGenerator.generate(func, hidden_params=hidden_params or [])
 			except Exception as e:
 			except Exception as e:
 				logger.error(f"Failed to generate schema for {func_name}: {e}")
 				logger.error(f"Failed to generate schema for {func_name}: {e}")
 				raise
 				raise
@@ -94,6 +98,8 @@ class ToolRegistry:
 			"func": func,
 			"func": func,
 			"schema": schema,
 			"schema": schema,
 			"url_patterns": url_patterns,
 			"url_patterns": url_patterns,
+			"hidden_params": hidden_params or [],
+			"inject_params": inject_params or {},
 			"ui_metadata": {
 			"ui_metadata": {
 				"requires_confirmation": requires_confirmation,
 				"requires_confirmation": requires_confirmation,
 				"editable_params": editable_params or [],
 				"editable_params": editable_params or [],
@@ -111,6 +117,29 @@ class ToolRegistry:
 			f"url_patterns={url_patterns or 'none'})"
 			f"url_patterns={url_patterns or 'none'})"
 		)
 		)
 
 
+	@staticmethod
+	def _resolve_key_path(context: Dict[str, Any], key_path: str) -> Any:
+		"""
+		从 context 中按路径取值。
+
+		支持 "obj.field" 格式:第一段从 context dict 取值,后续段用 getattr。
+		例如 "knowledge_config.default_tags" → context["knowledge_config"].default_tags
+
+		Args:
+			context: 上下文字典
+			key_path: 取值路径
+
+		Returns:
+			取到的值,路径无效返回 None
+		"""
+		parts = key_path.split(".")
+		value = context.get(parts[0])
+		for part in parts[1:]:
+			if value is None:
+				return None
+			value = getattr(value, part, None)
+		return value
+
 	def is_registered(self, tool_name: str) -> bool:
 	def is_registered(self, tool_name: str) -> bool:
 		"""检查工具是否已注册"""
 		"""检查工具是否已注册"""
 		return tool_name in self._tools
 		return tool_name in self._tools
@@ -177,7 +206,8 @@ class ToolRegistry:
 		arguments: Dict[str, Any],
 		arguments: Dict[str, Any],
 		uid: str = "",
 		uid: str = "",
 		context: Optional[Dict[str, Any]] = None,
 		context: Optional[Dict[str, Any]] = None,
-		sensitive_data: Optional[Dict[str, Any]] = None
+		sensitive_data: Optional[Dict[str, Any]] = None,
+		inject_values: Optional[Dict[str, Any]] = None
 	) -> str:
 	) -> str:
 		"""
 		"""
 		执行工具调用
 		执行工具调用
@@ -204,6 +234,7 @@ class ToolRegistry:
 
 
 		try:
 		try:
 			func = self._tools[name]["func"]
 			func = self._tools[name]["func"]
+			tool_info = self._tools[name]
 
 
 			# 处理敏感数据占位符
 			# 处理敏感数据占位符
 			if sensitive_data:
 			if sensitive_data:
@@ -215,14 +246,50 @@ class ToolRegistry:
 			kwargs = {**arguments}
 			kwargs = {**arguments}
 			sig = inspect.signature(func)
 			sig = inspect.signature(func)
 
 
-			# 注入 uid(如果函数接受)
-			if "uid" in sig.parameters:
+			# 注入隐藏参数(hidden_params)
+			hidden_params = tool_info.get("hidden_params", [])
+			if "uid" in hidden_params and "uid" in sig.parameters:
 				kwargs["uid"] = uid
 				kwargs["uid"] = uid
-
-			# 注入 context(如果函数接受)
-			if "context" in sig.parameters:
+			if "context" in hidden_params and "context" in sig.parameters:
 				kwargs["context"] = context
 				kwargs["context"] = context
 
 
+			# 注入参数(inject_params)
+			inject_params = tool_info.get("inject_params", {})
+			for param_name, rule in inject_params.items():
+				if param_name not in sig.parameters:
+					continue
+
+				if not isinstance(rule, dict) or "mode" not in rule:
+					# 兼容旧格式:直接值或 callable
+					if param_name not in kwargs or kwargs[param_name] is None:
+						kwargs[param_name] = rule() if callable(rule) else rule
+					continue
+
+				mode = rule["mode"]
+				key_path = rule.get("key")
+
+				# 从 context 中按路径取值
+				value = self._resolve_key_path(context, key_path) if key_path and context else None
+
+				if value is None:
+					continue
+
+				if mode == "default":
+					# 默认值模式:LLM 未提供则注入
+					if param_name not in kwargs or kwargs[param_name] is None:
+						kwargs[param_name] = value
+				elif mode == "merge":
+					# 合并模式:框架值始终保留,LLM 可追加新内容
+					llm_value = kwargs.get(param_name)
+					if isinstance(value, dict):
+						# dict: LLM 追加新 key,同名 key 以框架值为准
+						kwargs[param_name] = {**(llm_value or {}), **value}
+					elif isinstance(value, list):
+						# list: 合并去重
+						kwargs[param_name] = list(set((llm_value or []) + value))
+					else:
+						kwargs[param_name] = value
+
 			# 执行函数
 			# 执行函数
 			if inspect.iscoroutinefunction(func):
 			if inspect.iscoroutinefunction(func):
 				result = await func(**kwargs)
 				result = await func(**kwargs)
@@ -415,7 +482,9 @@ def tool(
 	requires_confirmation: bool = False,
 	requires_confirmation: bool = False,
 	editable_params: Optional[List[str]] = None,
 	editable_params: Optional[List[str]] = None,
 	display: Optional[Dict[str, Dict[str, Any]]] = None,
 	display: Optional[Dict[str, Dict[str, Any]]] = None,
-	url_patterns: Optional[List[str]] = None
+	url_patterns: Optional[List[str]] = None,
+	hidden_params: Optional[List[str]] = None,
+	inject_params: Optional[Dict[str, Any]] = None
 ):
 ):
 	"""
 	"""
 	工具装饰器 - 自动注册工具并生成 Schema
 	工具装饰器 - 自动注册工具并生成 Schema
@@ -427,9 +496,15 @@ def tool(
 		editable_params: 允许用户编辑的参数列表
 		editable_params: 允许用户编辑的参数列表
 		display: i18n 展示信息
 		display: i18n 展示信息
 		url_patterns: URL 模式列表(如 ["*.google.com"],None = 无限制)
 		url_patterns: URL 模式列表(如 ["*.google.com"],None = 无限制)
+		hidden_params: 隐藏参数列表(不生成 schema,LLM 看不到)
+		inject_params: 注入参数规则 {param_name: injector_func}
 
 
 	Example:
 	Example:
 		@tool(
 		@tool(
+			hidden_params=["context", "uid"],
+			inject_params={
+				"owner": lambda ctx: ctx.config.knowledge.get_owner(),
+			},
 			editable_params=["query"],
 			editable_params=["query"],
 			url_patterns=["*.google.com"],
 			url_patterns=["*.google.com"],
 			display={
 			display={
@@ -437,7 +512,13 @@ def tool(
 				"en": {"name": "Search Notes", "params": {"query": "Query"}}
 				"en": {"name": "Search Notes", "params": {"query": "Query"}}
 			}
 			}
 		)
 		)
-		async def search_blocks(query: str, limit: int = 10, uid: str = "") -> str:
+		async def search_blocks(
+			query: str,
+			limit: int = 10,
+			owner: Optional[str] = None,
+			context: Optional[ToolContext] = None,
+			uid: str = ""
+		) -> str:
 			'''搜索用户的笔记块'''
 			'''搜索用户的笔记块'''
 			...
 			...
 	"""
 	"""
@@ -448,7 +529,9 @@ def tool(
 			requires_confirmation=requires_confirmation,
 			requires_confirmation=requires_confirmation,
 			editable_params=editable_params,
 			editable_params=editable_params,
 			display=display,
 			display=display,
-			url_patterns=url_patterns
+			url_patterns=url_patterns,
+			hidden_params=hidden_params,
+			inject_params=inject_params
 		)
 		)
 		return func
 		return func
 
 

+ 15 - 5
agent/tools/schema.py

@@ -68,16 +68,19 @@ class SchemaGenerator:
     }
     }
 
 
     @classmethod
     @classmethod
-    def generate(cls, func: callable) -> Dict[str, Any]:
+    def generate(cls, func: callable, hidden_params: Optional[List[str]] = None) -> Dict[str, Any]:
         """
         """
         从函数生成 OpenAI Tool Schema
         从函数生成 OpenAI Tool Schema
 
 
         Args:
         Args:
             func: 要生成 Schema 的函数
             func: 要生成 Schema 的函数
+            hidden_params: 隐藏参数列表(不生成 schema)
 
 
         Returns:
         Returns:
             OpenAI Tool Schema(JSON 格式)
             OpenAI Tool Schema(JSON 格式)
         """
         """
+        hidden_params = hidden_params or []
+
         # 解析函数签名
         # 解析函数签名
         sig = inspect.signature(func)
         sig = inspect.signature(func)
         func_name = func.__name__
         func_name = func.__name__
@@ -98,11 +101,11 @@ class SchemaGenerator:
 
 
         for param_name, param in sig.parameters.items():
         for param_name, param in sig.parameters.items():
             # 跳过特殊参数
             # 跳过特殊参数
-            if param_name in ["self", "cls", "kwargs", "context"]:
+            if param_name in ["self", "cls", "kwargs"]:
                 continue
                 continue
 
 
-            # 跳过 uid(由框架自动注入)
-            if param_name == "uid":
+            # 跳过隐藏参数
+            if param_name in hidden_params:
                 continue
                 continue
 
 
             # 获取类型注解
             # 获取类型注解
@@ -184,6 +187,13 @@ class SchemaGenerator:
         if python_type in cls.TYPE_MAP:
         if python_type in cls.TYPE_MAP:
             return {"type": cls.TYPE_MAP[python_type]}
             return {"type": cls.TYPE_MAP[python_type]}
 
 
+        # 检查是否是 Protocol(如 ToolContext)
+        # Protocol 类型用于依赖注入,不应出现在 schema 中
+        type_name = getattr(python_type, "__name__", str(python_type))
+        if "Protocol" in str(type(python_type)) or type_name in ("ToolContext",):
+            logger.debug(f"Skipping Protocol type {python_type} (used for dependency injection)")
+            return {}
+
         # 默认为 string
         # 默认为 string
-        logger.warning(f"Unknown type {python_type}, defaulting to string")
+        logger.debug(f"Unknown type {python_type}, defaulting to string")
         return {"type": "string"}
         return {"type": "string"}

+ 12 - 55
agent/trace/compaction.py

@@ -18,6 +18,11 @@ from typing import List, Dict, Any, Optional, Set
 
 
 from .goal_models import GoalTree
 from .goal_models import GoalTree
 from .models import Message
 from .models import Message
+from agent.core.prompts import (
+    COMPRESSION_EVAL_PROMPT_TEMPLATE,
+    REFLECT_PROMPT,
+    build_compression_eval_prompt,
+)
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
@@ -84,6 +89,7 @@ class CompressionConfig:
     max_tokens: int = 0                # 最大 token 数(0 = 自动:context_window * 0.5)
     max_tokens: int = 0                # 最大 token 数(0 = 自动:context_window * 0.5)
     threshold_ratio: float = 0.5       # 触发压缩的阈值 = context_window 的比例
     threshold_ratio: float = 0.5       # 触发压缩的阈值 = context_window 的比例
     keep_recent_messages: int = 10     # Level 1 中始终保留最近 N 条消息
     keep_recent_messages: int = 10     # Level 1 中始终保留最近 N 条消息
+    max_messages: int = 50             # 最大消息数(超过此数量触发压缩,0 = 禁用)
 
 
     def get_max_tokens(self, model: str) -> int:
     def get_max_tokens(self, model: str) -> int:
         """获取实际的 max_tokens(如果为 0 则自动计算)"""
         """获取实际的 max_tokens(如果为 0 则自动计算)"""
@@ -296,71 +302,22 @@ def needs_level2_compression(
 
 
 
 
 # ===== Level 2: 压缩 Prompt =====
 # ===== Level 2: 压缩 Prompt =====
+# 注意:这些 prompt 已迁移到 agent.core.prompts
+# COMPRESSION_EVAL_PROMPT 和 REFLECT_PROMPT 现在从 prompts.py 导入
 
 
-COMPRESSION_EVAL_PROMPT = """请对以上对话历史进行压缩总结,并评价所引用的历史经验。
-### 任务 1:评价已用经验
-本次任务参考了以下经验内容:{ex_reference_list}
-
-请对比“经验建议”与“实际执行轨迹”,给出三色打分:
-[[EVALUATION]]
-ID: ex_xxx | Result: helpful/harmful/mixed | Reason: [优点]... [局限/修正]...
-
-### 任务 2:对话历史摘要
-要求:
-1. 保留关键决策、结论和产出(如创建的文件、修改的代码、得出的分析结论)
-2. 保留重要的上下文(如用户的要求、约束条件、之前的讨论结果)
-3. 省略中间探索过程、重复的工具调用细节
-4. 使用结构化格式(标题 + 要点 + 相关资源引用,若有)
-5. 控制在 2000 字以内
-格式要求:
-[[SUMMARY]]
-(此处填写结构化的摘要内容)
-
-当前 GoalTree 状态:
-{goal_tree_prompt}
-"""
-
-REFLECT_PROMPT = """请回顾以上整个执行过程,提取有价值的经验教训。
-你必须将经验与当前的任务意图(Intent)和环境状态(State)挂钩,以便未来精准检索。
-关注以下方面:
-1. 人工干预:用户中途的指令是否说明了原来的执行过程哪里有问题
-2. 弯路:哪些尝试是不必要的,有没有更直接的方法
-3. 好的决策:哪些判断和选择是正确的,值得记住
-4. 工具使用:哪些工具用法是高效的,哪些可以改进
-
-输出格式(严格遵守):
-- 在每条经验前加一个[]中添加自定义的标签,标签要求总结实际的内容为若干词语,包括:
-    - intent: 当前的goal
-    - state: 环境状态(如果与工具相关,可以在标签中加入工具的名称)
-- 经验标签可用自然语言描述
-- 每条经验单独成段,格式固定为:- 当 [条件] 时,应该 [动作](原因:[一句话说明])。具体案例:[案例]
-- 条目之间用一个空行分隔
-- 不输出任何标题、分类、编号、分隔线或其他结构
-- 不使用 markdown 加粗、表格、代码块等格式
-- 每条经验自包含,读者无需上下文即可理解
-- 只提取最有价值的 5-10 条,宁少勿滥
-
-示例(仅供参考格式,不要复制内容):
-- [intent:示例生成 state:用户提醒,指定样本] 当用户说"给我示例"时,应该用真实数据而不是编造(原因:编造的示例无法验证质量)。具体案例:training_samples.json 中的示例全是 LLM 自己编造的,用户明确要求"基于我指定的样本"。
-"""
 
 
-
-def build_compression_prompt(goal_tree: Optional[GoalTree], used_ex_ids: Optional[List[str]] = None) -> str:
-    """构建 Level 2 压缩 prompt(含经验评估)"""
+def build_compression_prompt(goal_tree: Optional[GoalTree]) -> str:
+    """构建 Level 2 压缩 prompt"""
     goal_prompt = ""
     goal_prompt = ""
     if goal_tree:
     if goal_tree:
         goal_prompt = goal_tree.to_prompt(include_summary=True)
         goal_prompt = goal_tree.to_prompt(include_summary=True)
 
 
-    ex_reference = "无(本次未引用历史经验)"
-    if used_ex_ids:
-        ex_reference = ", ".join(used_ex_ids)
-
-    return COMPRESSION_EVAL_PROMPT.format(
+    return build_compression_eval_prompt(
         goal_tree_prompt=goal_prompt,
         goal_tree_prompt=goal_prompt,
-        ex_reference_list=ex_reference,
     )
     )
 
 
 
 
 def build_reflect_prompt() -> str:
 def build_reflect_prompt() -> str:
     """构建反思 prompt"""
     """构建反思 prompt"""
     return REFLECT_PROMPT
     return REFLECT_PROMPT
+

+ 144 - 0
agent/trace/examples_api.py

@@ -0,0 +1,144 @@
+"""
+Examples API - 提供 examples 项目列表和 prompt 读取接口
+"""
+
+import os
+from typing import List, Optional
+from pathlib import Path
+from fastapi import APIRouter, HTTPException
+from pydantic import BaseModel
+
+
+router = APIRouter(prefix="/api/examples", tags=["examples"])
+
+
+class ExampleProject(BaseModel):
+    """Example 项目信息"""
+    name: str
+    path: str
+    has_prompt: bool
+
+
+class ExampleListResponse(BaseModel):
+    """Example 列表响应"""
+    projects: List[ExampleProject]
+
+
+class PromptResponse(BaseModel):
+    """Prompt 响应"""
+    system_prompt: str
+    user_prompt: str
+    model: Optional[str] = None
+    temperature: Optional[float] = None
+
+
+# 配置 examples 目录路径
+EXAMPLES_DIR = Path("examples")
+
+
+@router.get("", response_model=ExampleListResponse)
+async def list_examples():
+    """
+    列出所有 example 项目
+
+    扫描 examples 目录,返回所有子目录及其 prompt 文件状态
+    """
+    if not EXAMPLES_DIR.exists():
+        return ExampleListResponse(projects=[])
+
+    projects = []
+    for item in EXAMPLES_DIR.iterdir():
+        if item.is_dir():
+            # 检查是否有 prompt 文件
+            prompt_file = item / "production.prompt"
+            has_prompt = prompt_file.exists()
+
+            projects.append(ExampleProject(
+                name=item.name,
+                path=str(item),
+                has_prompt=has_prompt
+            ))
+
+    # 按名称排序
+    projects.sort(key=lambda x: x.name)
+
+    return ExampleListResponse(projects=projects)
+
+
+@router.get("/{project_name}/prompt", response_model=PromptResponse)
+async def get_example_prompt(project_name: str):
+    """
+    获取指定 example 项目的 prompt
+
+    读取 production.prompt 文件,解析 frontmatter 和内容
+    """
+    project_path = EXAMPLES_DIR / project_name
+    if not project_path.exists() or not project_path.is_dir():
+        raise HTTPException(status_code=404, detail=f"Project not found: {project_name}")
+
+    prompt_file = project_path / "production.prompt"
+    if not prompt_file.exists():
+        raise HTTPException(status_code=404, detail=f"Prompt file not found for project: {project_name}")
+
+    try:
+        content = prompt_file.read_text(encoding="utf-8")
+
+        # 解析 frontmatter 和内容
+        system_prompt = ""
+        user_prompt = ""
+        model = None
+        temperature = None
+
+        # 检查是否有 frontmatter
+        if content.startswith("---"):
+            parts = content.split("---", 2)
+            if len(parts) >= 3:
+                frontmatter = parts[1].strip()
+                body = parts[2].strip()
+
+                # 解析 frontmatter
+                for line in frontmatter.split("\n"):
+                    if ":" in line:
+                        key, value = line.split(":", 1)
+                        key = key.strip()
+                        value = value.strip()
+                        if key == "model":
+                            model = value
+                        elif key == "temperature":
+                            try:
+                                temperature = float(value)
+                            except ValueError:
+                                pass
+            else:
+                body = content
+        else:
+            body = content
+
+        # 解析 $system$ 和 $user$ 部分
+        if "$system$" in body:
+            parts = body.split("$system$", 1)
+            if len(parts) > 1:
+                rest = parts[1]
+                if "$user$" in rest:
+                    system_part, user_part = rest.split("$user$", 1)
+                    system_prompt = system_part.strip()
+                    user_prompt = user_part.strip()
+                else:
+                    system_prompt = rest.strip()
+        elif "$user$" in body:
+            parts = body.split("$user$", 1)
+            if len(parts) > 1:
+                user_prompt = parts[1].strip()
+        else:
+            # 没有标记,全部作为 user_prompt
+            user_prompt = body.strip()
+
+        return PromptResponse(
+            system_prompt=system_prompt,
+            user_prompt=user_prompt,
+            model=model,
+            temperature=temperature
+        )
+
+    except Exception as e:
+        raise HTTPException(status_code=500, detail=f"Failed to read prompt file: {str(e)}")

+ 14 - 0
agent/trace/goal_models.py

@@ -70,6 +70,9 @@ class Goal:
     self_stats: GoalStats = field(default_factory=GoalStats)          # 自身统计(仅直接关联的 messages)
     self_stats: GoalStats = field(default_factory=GoalStats)          # 自身统计(仅直接关联的 messages)
     cumulative_stats: GoalStats = field(default_factory=GoalStats)    # 累计统计(自身 + 所有后代)
     cumulative_stats: GoalStats = field(default_factory=GoalStats)    # 累计统计(自身 + 所有后代)
 
 
+    # 相关知识(自动检索注入)
+    knowledge: Optional[List[Dict[str, Any]]] = None                  # 相关知识列表
+
     created_at: datetime = field(default_factory=datetime.now)
     created_at: datetime = field(default_factory=datetime.now)
 
 
     def to_dict(self) -> Dict[str, Any]:
     def to_dict(self) -> Dict[str, Any]:
@@ -87,6 +90,7 @@ class Goal:
             "sub_trace_metadata": self.sub_trace_metadata,
             "sub_trace_metadata": self.sub_trace_metadata,
             "self_stats": self.self_stats.to_dict(),
             "self_stats": self.self_stats.to_dict(),
             "cumulative_stats": self.cumulative_stats.to_dict(),
             "cumulative_stats": self.cumulative_stats.to_dict(),
+            "knowledge": self.knowledge,
             "created_at": self.created_at.isoformat() if self.created_at else None,
             "created_at": self.created_at.isoformat() if self.created_at else None,
         }
         }
 
 
@@ -118,6 +122,7 @@ class Goal:
             sub_trace_metadata=data.get("sub_trace_metadata"),
             sub_trace_metadata=data.get("sub_trace_metadata"),
             self_stats=self_stats,
             self_stats=self_stats,
             cumulative_stats=cumulative_stats,
             cumulative_stats=cumulative_stats,
+            knowledge=data.get("knowledge"),
             created_at=created_at or datetime.now(),
             created_at=created_at or datetime.now(),
         )
         )
 
 
@@ -395,6 +400,15 @@ class GoalTree:
             if goal.summary and (include_summary or goal.id in current_path):
             if goal.summary and (include_summary or goal.id in current_path):
                 result.append(f"{prefix}    → {goal.summary}")
                 result.append(f"{prefix}    → {goal.summary}")
 
 
+            # 显示相关知识:仅在当前焦点 goal 显示
+            if goal.id == self.current_id and goal.knowledge:
+                result.append(f"{prefix}    📚 相关知识 ({len(goal.knowledge)} 条):")
+                for idx, k in enumerate(goal.knowledge[:3], 1):
+                    k_id = k.get('id', 'N/A')
+                    # 将多行内容压缩为单行摘要
+                    k_content = k.get('content', '').replace('\n', ' ').strip()[:80]
+                    result.append(f"{prefix}       {idx}. [{k_id}] {k_content}...")
+
             # 递归处理子目标
             # 递归处理子目标
             children = self.get_children(goal.id)
             children = self.get_children(goal.id)
 
 

+ 86 - 10
agent/trace/goal_tool.py

@@ -4,18 +4,91 @@ Goal 工具 - 计划管理
 提供 goal 工具供 LLM 管理执行计划。
 提供 goal 工具供 LLM 管理执行计划。
 """
 """
 
 
+import logging
 from typing import Optional, List, TYPE_CHECKING
 from typing import Optional, List, TYPE_CHECKING
 
 
 from agent.tools import tool
 from agent.tools import tool
 
 
 if TYPE_CHECKING:
 if TYPE_CHECKING:
-    from .goal_models import GoalTree
+    from .goal_models import GoalTree, Goal
     from .protocols import TraceStore
     from .protocols import TraceStore
 
 
+logger = logging.getLogger(__name__)
+
+
+# ===== 知识注入 =====
+
+async def inject_knowledge_for_goal(
+    goal: "Goal",
+    tree: "GoalTree",
+    store: Optional["TraceStore"] = None,
+    trace_id: Optional[str] = None,
+    knowledge_config: Optional[dict] = None,
+) -> Optional[str]:
+    """
+    为指定 goal 注入相关知识。
+
+    Args:
+        goal: 目标对象
+        tree: GoalTree
+        store: TraceStore(用于持久化)
+        trace_id: Trace ID
+        knowledge_config: 知识管理配置(KnowledgeConfig 对象)
+
+    Returns:
+        注入结果描述(如 "📚 已注入 3 条相关知识"),无结果返回 None
+    """
+    # 检查是否启用知识注入
+    if knowledge_config and not getattr(knowledge_config, 'enable_injection', True):
+        logger.debug(f"[Knowledge Inject] 知识注入已禁用,跳过")
+        return None
+
+    try:
+        from agent.tools.builtin.knowledge import knowledge_search
+
+        logger.info(f"[Knowledge Inject] goal: {goal.id}, query: {goal.description[:80]}")
+
+        # 从配置中获取搜索参数
+        search_types = None
+        search_owner = None
+        if knowledge_config:
+            search_types = getattr(knowledge_config, 'default_search_types', None)
+            search_owner = getattr(knowledge_config, 'default_search_owner', None) or None
+
+        knowledge_result = await knowledge_search(
+            query=goal.description,
+            top_k=3,
+            min_score=3,
+            types=search_types,
+            owner=search_owner,
+            context=None
+        )
+
+        logger.debug(f"[Knowledge Inject] result type: {type(knowledge_result)}, metadata: {getattr(knowledge_result, 'metadata', None)}")
+
+        if knowledge_result.metadata and knowledge_result.metadata.get("items"):
+            goal.knowledge = knowledge_result.metadata["items"]
+            knowledge_count = len(goal.knowledge)
+            logger.info(f"[Knowledge Inject] 注入 {knowledge_count} 条知识到 goal {goal.id}")
+
+            if store and trace_id:
+                await store.update_goal_tree(trace_id, tree)
+
+            return f"📚 已注入 {knowledge_count} 条相关知识"
+        else:
+            goal.knowledge = []
+            logger.info(f"[Knowledge Inject] 未找到相关知识")
+            return None
+
+    except Exception as e:
+        logger.warning(f"[Knowledge Inject] 知识注入失败: {e}")
+        goal.knowledge = []
+        return None
+
 
 
 # ===== LLM 可调用的 goal 工具 =====
 # ===== LLM 可调用的 goal 工具 =====
 
 
-@tool(description="管理执行计划,添加/完成/放弃目标,切换焦点")
+@tool(description="管理执行计划,添加/完成/放弃目标,切换焦点", hidden_params=["context"])
 async def goal(
 async def goal(
     add: Optional[str] = None,
     add: Optional[str] = None,
     reason: Optional[str] = None,
     reason: Optional[str] = None,
@@ -47,9 +120,10 @@ async def goal(
     if tree is None:
     if tree is None:
         return "错误:GoalTree 未初始化"
         return "错误:GoalTree 未初始化"
 
 
-    # 从 context 获取 store 和 trace_id
+    # 从 context 获取 store、trace_id 和 knowledge_config
     store = context.get("store") if context else None
     store = context.get("store") if context else None
     trace_id = context.get("trace_id") if context else None
     trace_id = context.get("trace_id") if context else None
+    knowledge_config = context.get("knowledge_config") if context else None
 
 
     return await goal_tool(
     return await goal_tool(
         tree=tree,
         tree=tree,
@@ -61,7 +135,8 @@ async def goal(
         under=under,
         under=under,
         done=done,
         done=done,
         abandon=abandon,
         abandon=abandon,
-        focus=focus
+        focus=focus,
+        knowledge_config=knowledge_config
     )
     )
 
 
 
 
@@ -79,6 +154,7 @@ async def goal_tool(
     done: Optional[str] = None,
     done: Optional[str] = None,
     abandon: Optional[str] = None,
     abandon: Optional[str] = None,
     focus: Optional[str] = None,
     focus: Optional[str] = None,
+    knowledge_config: Optional[object] = None,
 ) -> str:
 ) -> str:
     """
     """
     管理执行计划。
     管理执行计划。
@@ -94,6 +170,7 @@ async def goal_tool(
         done: 完成当前目标,值为 summary
         done: 完成当前目标,值为 summary
         abandon: 放弃当前目标,值为原因
         abandon: 放弃当前目标,值为原因
         focus: 切换焦点到指定 ID
         focus: 切换焦点到指定 ID
+        knowledge_config: 知识管理配置(KnowledgeConfig 对象)
 
 
     Returns:
     Returns:
         更新后的计划状态文本
         更新后的计划状态文本
@@ -135,6 +212,11 @@ async def goal_tool(
         display_id = tree._generate_display_id(goal)
         display_id = tree._generate_display_id(goal)
         changes.append(f"切换焦点: {display_id}. {goal.description}")
         changes.append(f"切换焦点: {display_id}. {goal.description}")
 
 
+        # 自动注入知识
+        inject_msg = await inject_knowledge_for_goal(goal, tree, store, trace_id, knowledge_config)
+        if inject_msg:
+            changes.append(inject_msg)
+
     # 3. 处理 abandon(放弃当前目标)
     # 3. 处理 abandon(放弃当前目标)
     if abandon is not None:
     if abandon is not None:
         if not tree.current_id:
         if not tree.current_id:
@@ -200,12 +282,6 @@ async def goal_tool(
                 for goal in new_goals:
                 for goal in new_goals:
                     await store.add_goal(trace_id, goal)
                     await store.add_goal(trace_id, goal)
 
 
-            # 如果没有焦点且添加了目标,自动 focus 到第一个新目标
-            if not tree.current_id and new_goals:
-                tree.focus(new_goals[0].id)
-                display_id = tree._generate_display_id(new_goals[0])
-                changes.append(f"自动切换焦点: {display_id}")
-
     # 将完整内存树状态(含 current_id)同步到存储,
     # 将完整内存树状态(含 current_id)同步到存储,
     # 因为 store.add_goal / update_goal 各自从磁盘加载,不包含 focus 等内存变更
     # 因为 store.add_goal / update_goal 各自从磁盘加载,不包含 focus 等内存变更
     if store and trace_id and changes:
     if store and trace_id and changes:

+ 114 - 0
agent/trace/logs_websocket.py

@@ -0,0 +1,114 @@
+"""
+Logs WebSocket - 实时推送后端日志到前端
+"""
+
+import asyncio
+import logging
+from typing import Set
+from fastapi import APIRouter, WebSocket, WebSocketDisconnect
+from datetime import datetime
+
+
+router = APIRouter(prefix="/api/logs", tags=["logs"])
+
+
+# 存储所有连接的WebSocket客户端
+_clients: Set[WebSocket] = set()
+
+
+class WebSocketLogHandler(logging.Handler):
+    """自定义日志处理器,将日志推送到WebSocket客户端"""
+
+    def emit(self, record: logging.LogRecord):
+        """发送日志记录到所有连接的客户端"""
+        try:
+            log_entry = self.format(record)
+            # 构造日志消息
+            message = {
+                "timestamp": datetime.now().isoformat(),
+                "level": record.levelname,
+                "name": record.name,
+                "message": log_entry,
+            }
+
+            # uvicorn 热重载线程等非主线程打印日志时,可能没有 running loop
+            try:
+                loop = asyncio.get_running_loop()
+                # 如果能在当前循环找到,说明在协程中,安全地抛出一个 task
+                loop.create_task(broadcast_log(message))
+            except RuntimeError:
+                # 找不到 running loop 说明不是在 async 上下文下,我们需要新开一个临时事件循环发消息
+                # 或者更简单的方式是:在没循环时就暂时丢弃/降级,因为没人监听的时候发了也没意义。
+                # 由于这只是实时看日志的前端需求,如果在纯同步上下文中可以采用 run_coroutine_threadsafe 交给一个已知的 loop(但这里拿不到全局 loop)。
+                # 我们这里使用兼容写法:短暂新开 loop 去触发一次网络写入(或者最稳妥的是:当前线程没有客户端也就没必要强求广播了)。
+                if _clients:
+                    asyncio.run(broadcast_log(message))
+                    
+        except Exception:
+            self.handleError(record)
+
+
+async def broadcast_log(message: dict):
+    """广播日志消息到所有连接的客户端"""
+    disconnected = set()
+    for client in _clients:
+        try:
+            await client.send_json(message)
+        except Exception:
+            disconnected.add(client)
+
+    # 移除断开连接的客户端
+    for client in disconnected:
+        _clients.discard(client)
+
+
+@router.websocket("/watch")
+async def logs_websocket(websocket: WebSocket):
+    """
+    日志WebSocket端点
+
+    客户端连接后,实时接收后端日志
+    """
+    await websocket.accept()
+    _clients.add(websocket)
+
+    try:
+        # 发送欢迎消息
+        await websocket.send_json({
+            "timestamp": datetime.now().isoformat(),
+            "level": "INFO",
+            "name": "logs_websocket",
+            "message": "Connected to logs stream",
+        })
+
+        # 保持连接,等待客户端断开
+        while True:
+            # 接收客户端消息(用于保持连接)
+            await websocket.receive_text()
+    except WebSocketDisconnect:
+        pass
+    finally:
+        _clients.discard(websocket)
+
+
+def setup_websocket_logging(level=logging.INFO):
+    """
+    设置WebSocket日志处理器
+
+    将根日志器的日志推送到WebSocket客户端
+    """
+    handler = WebSocketLogHandler()
+    handler.setLevel(level)
+
+    # 设置日志格式
+    formatter = logging.Formatter(
+        "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
+        datefmt="%Y-%m-%d %H:%M:%S"
+    )
+    handler.setFormatter(formatter)
+
+    # 添加到根日志器
+    root_logger = logging.getLogger()
+    root_logger.addHandler(handler)
+
+    return handler

+ 44 - 4
agent/trace/models.py

@@ -86,6 +86,7 @@ class Trace:
     # 时间
     # 时间
     created_at: datetime = field(default_factory=datetime.now)
     created_at: datetime = field(default_factory=datetime.now)
     completed_at: Optional[datetime] = None
     completed_at: Optional[datetime] = None
+    last_activity_at: datetime = field(default_factory=datetime.now)  # 最后活动时间(用于判断是否真正运行中)
 
 
     @classmethod
     @classmethod
     def create(
     def create(
@@ -100,6 +101,21 @@ class Trace:
             **kwargs
             **kwargs
         )
         )
 
 
+    @classmethod
+    def from_dict(cls, data: Dict[str, Any]) -> "Trace":
+        """从字典创建 Trace(处理日期字段反序列化)"""
+        from dateutil import parser
+
+        # 处理日期字段
+        if "created_at" in data and isinstance(data["created_at"], str):
+            data["created_at"] = parser.isoparse(data["created_at"])
+        if "completed_at" in data and isinstance(data["completed_at"], str):
+            data["completed_at"] = parser.isoparse(data["completed_at"])
+        if "last_activity_at" in data and isinstance(data["last_activity_at"], str):
+            data["last_activity_at"] = parser.isoparse(data["last_activity_at"])
+
+        return cls(**data)
+
     def to_dict(self) -> Dict[str, Any]:
     def to_dict(self) -> Dict[str, Any]:
         """转换为字典"""
         """转换为字典"""
         return {
         return {
@@ -133,6 +149,7 @@ class Trace:
             "error_message": self.error_message,
             "error_message": self.error_message,
             "created_at": self.created_at.isoformat() if self.created_at else None,
             "created_at": self.created_at.isoformat() if self.created_at else None,
             "completed_at": self.completed_at.isoformat() if self.completed_at else None,
             "completed_at": self.completed_at.isoformat() if self.completed_at else None,
+            "last_activity_at": self.last_activity_at.isoformat() if self.last_activity_at else None,
         }
         }
 
 
 
 
@@ -160,6 +177,10 @@ class Message:
     tool_call_id: Optional[str] = None   # tool 消息关联对应的 tool_call
     tool_call_id: Optional[str] = None   # tool 消息关联对应的 tool_call
     content: Any = None                  # 消息内容(和 LLM API 格式一致)
     content: Any = None                  # 消息内容(和 LLM API 格式一致)
 
 
+    # 侧分支标记
+    branch_type: Optional[Literal["compression", "reflection"]] = None  # 侧分支类型(None = 主路径)
+    branch_id: Optional[str] = None      # 侧分支 ID(同一侧分支的消息共享)
+
     # 元数据
     # 元数据
     prompt_tokens: Optional[int] = None  # 输入 tokens
     prompt_tokens: Optional[int] = None  # 输入 tokens
     completion_tokens: Optional[int] = None  # 输出 tokens
     completion_tokens: Optional[int] = None  # 输出 tokens
@@ -277,6 +298,12 @@ class Message:
         if "parent_sequence" not in filtered_data:
         if "parent_sequence" not in filtered_data:
             filtered_data["parent_sequence"] = None
             filtered_data["parent_sequence"] = None
 
 
+        # 向后兼容:旧消息没有侧分支字段
+        if "branch_type" not in filtered_data:
+            filtered_data["branch_type"] = None
+        if "branch_id" not in filtered_data:
+            filtered_data["branch_id"] = None
+
         return cls(**filtered_data)
         return cls(**filtered_data)
 
 
     @classmethod
     @classmethod
@@ -289,6 +316,8 @@ class Message:
         content: Any = None,
         content: Any = None,
         tool_call_id: Optional[str] = None,
         tool_call_id: Optional[str] = None,
         parent_sequence: Optional[int] = None,
         parent_sequence: Optional[int] = None,
+        branch_type: Optional[Literal["compression", "reflection"]] = None,
+        branch_id: Optional[str] = None,
         prompt_tokens: Optional[int] = None,
         prompt_tokens: Optional[int] = None,
         completion_tokens: Optional[int] = None,
         completion_tokens: Optional[int] = None,
         reasoning_tokens: Optional[int] = None,
         reasoning_tokens: Optional[int] = None,
@@ -311,6 +340,8 @@ class Message:
             content=content,
             content=content,
             description=description,
             description=description,
             tool_call_id=tool_call_id,
             tool_call_id=tool_call_id,
+            branch_type=branch_type,
+            branch_id=branch_id,
             prompt_tokens=prompt_tokens,
             prompt_tokens=prompt_tokens,
             completion_tokens=completion_tokens,
             completion_tokens=completion_tokens,
             reasoning_tokens=reasoning_tokens,
             reasoning_tokens=reasoning_tokens,
@@ -356,12 +387,21 @@ class Message:
                 if content.get("tool_calls"):
                 if content.get("tool_calls"):
                     tool_calls = content["tool_calls"]
                     tool_calls = content["tool_calls"]
                     if isinstance(tool_calls, list):
                     if isinstance(tool_calls, list):
-                        tool_names = []
+                        tool_descriptions = []
                         for tc in tool_calls:
                         for tc in tool_calls:
                             if isinstance(tc, dict) and tc.get("function", {}).get("name"):
                             if isinstance(tc, dict) and tc.get("function", {}).get("name"):
-                                tool_names.append(tc["function"]["name"])
-                        if tool_names:
-                            return f"tool call: {', '.join(tool_names)}"
+                                tool_name = tc["function"]["name"]
+                                # 提取参数并截断到 100 字符
+                                tool_args = tc["function"].get("arguments", "{}")
+                                if isinstance(tool_args, str):
+                                    args_str = tool_args
+                                else:
+                                    import json
+                                    args_str = json.dumps(tool_args, ensure_ascii=False)
+                                args_display = args_str[:100] + "..." if len(args_str) > 100 else args_str
+                                tool_descriptions.append(f"{tool_name}({args_display})")
+                        if tool_descriptions:
+                            return "tool call: " + ", ".join(tool_descriptions)
 
 
             # 如果 content 是字符串
             # 如果 content 是字符串
             if isinstance(content, str):
             if isinstance(content, str):

+ 233 - 83
agent/trace/run_api.py

@@ -67,6 +67,7 @@ class CreateRequest(BaseModel):
     tools: Optional[List[str]] = Field(None, description="工具白名单(None = 全部)")
     tools: Optional[List[str]] = Field(None, description="工具白名单(None = 全部)")
     name: Optional[str] = Field(None, description="任务名称(None = 自动生成)")
     name: Optional[str] = Field(None, description="任务名称(None = 自动生成)")
     uid: Optional[str] = Field(None)
     uid: Optional[str] = Field(None)
+    project_name: Optional[str] = Field(None, description="示例项目名称,若提供则动态加载其执行环境")
 
 
 
 
 class TraceRunRequest(BaseModel):
 class TraceRunRequest(BaseModel):
@@ -105,14 +106,22 @@ class ReflectResponse(BaseModel):
     reflection: str
     reflection: str
 
 
 
 
+class CompactResponse(BaseModel):
+    """压缩响应"""
+    trace_id: str
+    previous_count: int
+    new_count: int
+    message: str = ""
+
+
 # ===== 后台执行 =====
 # ===== 后台执行 =====
 
 
 _running_tasks: Dict[str, asyncio.Task] = {}
 _running_tasks: Dict[str, asyncio.Task] = {}
 
 
 
 
-async def _run_in_background(trace_id: str, messages: List[Dict], config):
+async def _run_in_background(trace_id: str, messages: List[Dict], config, runner_instance=None):
     """后台执行 agent,消费 run() 的所有 yield"""
     """后台执行 agent,消费 run() 的所有 yield"""
-    runner = _get_runner()
+    runner = runner_instance or _get_runner()
     try:
     try:
         async for _item in runner.run(messages=messages, config=config):
         async for _item in runner.run(messages=messages, config=config):
             pass  # WebSocket 广播由 runner 内部的 store 事件驱动
             pass  # WebSocket 广播由 runner 内部的 store 事件驱动
@@ -123,12 +132,12 @@ async def _run_in_background(trace_id: str, messages: List[Dict], config):
 
 
 
 
 async def _run_with_trace_signal(
 async def _run_with_trace_signal(
-    messages: List[Dict], config, trace_id_future: asyncio.Future,
+    messages: List[Dict], config, trace_id_future: asyncio.Future, runner_instance=None
 ):
 ):
     """后台执行 agent,通过 Future 将 trace_id 传回给等待的 endpoint"""
     """后台执行 agent,通过 Future 将 trace_id 传回给等待的 endpoint"""
     from agent.trace.models import Trace
     from agent.trace.models import Trace
 
 
-    runner = _get_runner()
+    runner = runner_instance or _get_runner()
     trace_id: Optional[str] = None
     trace_id: Optional[str] = None
     try:
     try:
         async for item in runner.run(messages=messages, config=config):
         async for item in runner.run(messages=messages, config=config):
@@ -155,23 +164,60 @@ async def create_and_run(req: CreateRequest):
     立即返回 trace_id,后台异步执行。
     立即返回 trace_id,后台异步执行。
     通过 WebSocket /api/traces/{trace_id}/watch 监听实时更新。
     通过 WebSocket /api/traces/{trace_id}/watch 监听实时更新。
     """
     """
+    import importlib
     from agent.core.runner import RunConfig
     from agent.core.runner import RunConfig
 
 
-    _get_runner()  # 验证 Runner 已配置
-
-    config = RunConfig(
-        model=req.model,
-        temperature=req.temperature,
-        max_iterations=req.max_iterations,
-        tools=req.tools,
-        name=req.name,
-        uid=req.uid,
-    )
+    runner = None
+    config = None
+    messages = req.messages
+
+    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"):
+                # 获取该 example 专属的 runner, 带上下文 messages, 以及默认 config
+                runner, example_messages, default_config = await example_module.init_project_env(req.messages)
+                messages = example_messages
+                
+                # 合并请求配置和 example 默认配置
+                config = RunConfig(
+                    model=req.model or default_config.model,
+                    temperature=req.temperature if req.temperature is not None else default_config.temperature,
+                    max_iterations=req.max_iterations or default_config.max_iterations,
+                    tools=req.tools or default_config.tools,
+                    name=req.name or default_config.name,
+                    uid=req.uid or default_config.uid,
+                    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:
+            import traceback
+            logger.error(f"Unexpected error loading project environment for {req.project_name}:\n{traceback.format_exc()}")
+            
+    if not runner:
+        _get_runner()  # 验证全局默认 Runner 已配置
+        config = RunConfig(
+            model=req.model,
+            temperature=req.temperature,
+            max_iterations=req.max_iterations,
+            tools=req.tools,
+            name=req.name,
+            uid=req.uid,
+            context={"project_name": req.project_name} if req.project_name else {}
+        )
 
 
     # 启动后台执行,通过 Future 等待 trace_id(Phase 1 完成后即返回)
     # 启动后台执行,通过 Future 等待 trace_id(Phase 1 完成后即返回)
     trace_id_future: asyncio.Future[str] = asyncio.get_running_loop().create_future()
     trace_id_future: asyncio.Future[str] = asyncio.get_running_loop().create_future()
     task = asyncio.create_task(
     task = asyncio.create_task(
-        _run_with_trace_signal(req.messages, config, trace_id_future)
+        _run_with_trace_signal(messages, config, trace_id_future, runner_instance=runner)
     )
     )
 
 
     trace_id = await trace_id_future
     trace_id = await trace_id_future
@@ -298,6 +344,7 @@ async def run_trace(trace_id: str, req: TraceRunRequest):
     但缺少对应的 tool responses),框架会自动检测并调整插入位置,确保不会产生不一致的状态。
     但缺少对应的 tool responses),框架会自动检测并调整插入位置,确保不会产生不一致的状态。
     """
     """
     from agent.core.runner import RunConfig
     from agent.core.runner import RunConfig
+    import importlib
 
 
     runner = _get_runner()
     runner = _get_runner()
 
 
@@ -325,10 +372,56 @@ async def run_trace(trace_id: str, req: TraceRunRequest):
 
 
     # 检查是否已在运行
     # 检查是否已在运行
     if trace_id in _running_tasks and not _running_tasks[trace_id].done():
     if trace_id in _running_tasks and not _running_tasks[trace_id].done():
-        raise HTTPException(status_code=409, detail="Trace is already running")
+        # 竞态窗口修复:task 还没退出,但 store 里 trace 已经是 stopped/failed
+        # 这发生在 cancel_event 触发后 → store 更新 → task finally 块还未执行之间
+        # 此时可以安全地强制清除旧 task,允许续跑
+        store_trace = None
+        if runner.trace_store:
+            store_trace = await runner.trace_store.get_trace(trace_id)
+        if store_trace and store_trace.status in ("stopped", "failed", "completed"):
+            logger.info(
+                f"run_trace: task for {trace_id} not done yet but store status={store_trace.status!r}, "
+                "forcing cleanup to allow resume"
+            )
+            _running_tasks[trace_id].cancel()
+            _running_tasks.pop(trace_id, None)
+        else:
+            raise HTTPException(status_code=409, detail="Trace is already running")
+
+        # 检测 trace 中是否包含 project_name 环境定义
+        trace_context = trace.context or {}
+        project_name = trace_context.get("project_name")
+        
+        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"):
+                    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:
+                import traceback
+                logger.error(f"Unexpected error loading run.py environment for project {project_name} in trace {trace_id}:\n{traceback.format_exc()}")
 
 
     config = RunConfig(trace_id=trace_id, after_sequence=after_sequence)
     config = RunConfig(trace_id=trace_id, after_sequence=after_sequence)
-    task = asyncio.create_task(_run_in_background(trace_id, req.messages, config))
+
+    # 恢复运行时,将状态从 stopped 改回 running,并广播状态变化
+    if runner.trace_store and trace_id:
+        current_trace = await runner.trace_store.get_trace(trace_id)
+        if current_trace and current_trace.status == "stopped":
+            await runner.trace_store.update_trace(trace_id, status="running")
+            # 广播状态变化给前端
+            from agent.trace.websocket import broadcast_trace_status_changed
+            await broadcast_trace_status_changed(trace_id, "running")
+
+    task = asyncio.create_task(_run_in_background(trace_id, req.messages, config, runner_instance=runner))
     _running_tasks[trace_id] = task
     _running_tasks[trace_id] = task
 
 
     mode = "rewind" if after_sequence is not None else "continue"
     mode = "rewind" if after_sequence is not None else "continue"
@@ -370,14 +463,11 @@ async def reflect_trace(trace_id: str, req: ReflectRequest):
     """
     """
     触发反思
     触发反思
 
 
-    在 trace 末尾追加一条包含反思 prompt 的 user message,单轮无工具 LLM 调用获取反思结果,
-    将结果追加到 experiences 文件(默认 ./.cache/experiences.md)。
-
-    反思消息作为侧枝(side branch):运行前保存 head_sequence,运行后恢复(try/finally 保证)。
-    使用 max_iterations=1, tools=[] 确保反思不会产生副作用。
+    通过 force_side_branch="reflection" 触发侧分支多轮 agent 模式,
+    LLM 可以调用工具(如 knowledge_search, knowledge_save)进行多轮推理。
+    反思消息标记为侧分支(branch_type="reflection"),不在主路径上。
     """
     """
     from agent.core.runner import RunConfig
     from agent.core.runner import RunConfig
-    from agent.trace.compaction import build_reflect_prompt
 
 
     runner = _get_runner()
     runner = _get_runner()
 
 
@@ -393,85 +483,145 @@ async def reflect_trace(trace_id: str, req: ReflectRequest):
     if trace_id in _running_tasks and not _running_tasks[trace_id].done():
     if trace_id in _running_tasks and not _running_tasks[trace_id].done():
         raise HTTPException(status_code=409, detail="Cannot reflect on a running trace. Stop it first.")
         raise HTTPException(status_code=409, detail="Cannot reflect on a running trace. Stop it first.")
 
 
-    # 保存当前 head_sequence(反思完成后恢复,使反思消息成为侧枝)
-    saved_head_sequence = trace.head_sequence
+    # 使用 force_side_branch 触发反思侧分支
+    config = RunConfig(
+        trace_id=trace_id,
+        model=trace.model or "gpt-4o",
+        force_side_branch="reflection",
+        max_iterations=20,  # 给侧分支足够的轮次
+        enable_prompt_caching=True,
+    )
 
 
-    # 构建反思 prompt
-    prompt = build_reflect_prompt()
+    # 如果有 focus,可以通过追加消息传递(可选)
+    messages = []
     if req.focus:
     if req.focus:
-        prompt += f"\n\n请特别关注:{req.focus}"
-
-    # 以续跑方式运行:单轮无工具 LLM 调用
-    config = RunConfig(trace_id=trace_id, max_iterations=1, tools=[])
-    reflection_raw_text = ""
-    try:
-        result = await runner.run_result(
-            messages=[{"role": "user", "content": prompt}],
-            config=config,
-        )
-        reflection_raw_text = result.get("summary", "")
-    finally:
-        # 恢复 head_sequence(反思消息成为侧枝,不影响主路径)
-        await runner.trace_store.update_trace(trace_id, head_sequence=saved_head_sequence)
-
-    # --- 开始结构化解析与处理 ---
-    structured_entries = []
-    # 正则解析:匹配 - [intent:..., state:...] 经验内容
-    pattern = r"- \[(intent:.*?, state:.*?)\] (.*)"
-    matches = re.findall(pattern, reflection_raw_text)
-
-    for tags_str, content in matches:
-        # 生成唯一短 ID
-        ex_id = f"ex_{datetime.now().strftime('%m%d%H%M')}_{uuid.uuid4().hex[:4]}"
-        
-        # 提取标签详情
-        intent_match = re.search(r"intent:(.*?),", tags_str)
-        state_match = re.search(r"state:(.*)", tags_str)
-        
-        intents = [i.strip() for i in intent_match.group(1).split(",")] if intent_match else []
-        states = [s.strip() for s in state_match.group(1).split(",")] if state_match else []
-
-        # 构造符合文档规范的结构化条目
-        entry = f"""---
-id: {ex_id}
-trace_id: {trace_id}
-tags: {{intent: {intents}, state: {states}}}
-metrics: {{helpful: 1, harmful: 0}}
-created_at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
----
-- {content}
-- 引用策略 ID: [{ex_id}]
-"""
-        structured_entries.append(entry)
+        messages = [{"role": "user", "content": f"反思重点:{req.focus}"}]
 
 
-    # 追加到经验文件
-    if structured_entries:
-        experiences_path = getattr(runner, "experiences_path", "./.cache/experiences.md")
-        os.makedirs(os.path.dirname(experiences_path), exist_ok=True)
-        
-        with open(experiences_path, "a", encoding="utf-8") as f:
-            f.write("\n\n" + "\n\n".join(structured_entries))
-        
-        logger.info(f"Successfully extracted {len(structured_entries)} structured experiences.")
+    # 启动反思任务(后台执行)
+    task = asyncio.create_task(_run_trace_background(runner, messages, config))
+    _running_tasks[trace_id] = task
 
 
     return ReflectResponse(
     return ReflectResponse(
         trace_id=trace_id,
         trace_id=trace_id,
-        reflection=reflection_raw_text,
+        reflection="反思任务已启动,通过 WebSocket 监听实时更新",
+    )
+
+
+@router.post("/{trace_id}/compact", response_model=CompactResponse)
+async def compact_trace(trace_id: str):
+    """
+    压缩 Trace 的上下文 (Compact)
+
+    通过 force_side_branch="compression" 触发侧分支多轮 agent 模式,
+    LLM 可以调用工具(如 goal)进行多轮推理。
+    压缩消息标记为侧分支(branch_type="compression"),不在主路径上。
+    """
+    from agent.core.runner import RunConfig
+
+    runner = _get_runner()
+    if not runner.trace_store:
+        raise HTTPException(status_code=503, detail="TraceStore not configured")
+
+    # 验证 trace 存在
+    trace = await runner.trace_store.get_trace(trace_id)
+    if not trace:
+        raise HTTPException(status_code=404, detail=f"Trace not found: {trace_id}")
+
+    # 检查是否仍在运行
+    if trace_id in _running_tasks and not _running_tasks[trace_id].done():
+        raise HTTPException(status_code=409, detail="Cannot compact a running trace. Stop it first.")
+
+    # 使用 force_side_branch 触发压缩侧分支
+    config = RunConfig(
+        trace_id=trace_id,
+        model=trace.model or "gpt-4o",
+        force_side_branch="compression",
+        max_iterations=20,  # 给侧分支足够的轮次
+        enable_prompt_caching=True,
+    )
+
+    # 启动压缩任务(后台执行)
+    task = asyncio.create_task(_run_trace_background(runner, [], config))
+    _running_tasks[trace_id] = task
+
+    return CompactResponse(
+        trace_id=trace_id,
+        previous_count=0,  # 无法立即获取,需通过 WebSocket 监听
+        new_count=0,
+        message="压缩任务已启动,通过 WebSocket 监听实时更新",
     )
     )
 
 
 
 
 @router.get("/running", tags=["run"])
 @router.get("/running", tags=["run"])
 async def list_running():
 async def list_running():
-    """列出正在运行的 Trace"""
+    """列出正在运行的 Trace(包含活跃状态判断)"""
+    from datetime import datetime, timedelta
+
+    runner = _get_runner()
     running = []
     running = []
+
     for tid, task in list(_running_tasks.items()):
     for tid, task in list(_running_tasks.items()):
         if task.done():
         if task.done():
             _running_tasks.pop(tid, None)
             _running_tasks.pop(tid, None)
         else:
         else:
-            running.append(tid)
+            # 获取trace详情,检查最后活动时间
+            trace_info = {"trace_id": tid, "is_active": True}
+
+            if runner.trace_store:
+                try:
+                    trace = await runner.trace_store.get_trace(tid)
+                    if trace:
+                        # 判断是否真正活跃:最后活动时间在30秒内
+                        if hasattr(trace, 'last_activity_at') and trace.last_activity_at:
+                            time_since_activity = (datetime.now() - trace.last_activity_at).total_seconds()
+                            trace_info["is_active"] = time_since_activity < 30
+                            trace_info["seconds_since_activity"] = int(time_since_activity)
+                        trace_info["status"] = trace.status
+                except Exception:
+                    pass
+
+            running.append(trace_info)
+
     return {"running": running}
     return {"running": running}
 
 
 
 
+async def reconcile_traces():
+    """
+    状态对齐:启动时清理残留的 running 状态。
+    
+    当服务异常停止或重启后,磁盘上的 trace 状态可能仍显示为 running,
+    但对应的内存任务已不存在。本函数将其强制标记为 stopped。
+    """
+    runner = _get_runner()
+    if not runner or not runner.trace_store:
+        logger.warning("[Reconciliation] Runner or TraceStore not initialized, skipping.")
+        return
+
+    try:
+        # 获取所有 running 状态的 trace
+        running_traces = await runner.trace_store.list_traces(status="running", limit=1000)
+        if not running_traces:
+            return
+
+        count = 0
+        for trace in running_traces:
+            tid = trace.trace_id
+            # 如果不在活跃任务字典中(服务初次启动时此字典为空),则视为异常残留
+            if tid not in _running_tasks:
+                logger.info(f"[Reconciliation] Fixing trace {tid}: running -> stopped")
+                await runner.trace_store.update_trace(
+                    tid,
+                    status="stopped",
+                    result_summary="[Reconciliation] 任务由于服务重启或异常中断已自动停止。"
+                )
+                count += 1
+        
+        if count > 0:
+            logger.info(f"[Reconciliation] Successfully reconciled {count} traces.")
+    except Exception as e:
+        logger.error(f"[Reconciliation] Failed to reconcile traces: {e}")
+
+
 # ===== 经验 API =====
 # ===== 经验 API =====
 
 
 
 

+ 44 - 18
agent/trace/store.py

@@ -22,6 +22,7 @@ Sub-Trace 是完全独立的 Trace,有自己的目录:
 
 
 import json
 import json
 import os
 import os
+import logging
 from pathlib import Path
 from pathlib import Path
 from typing import Dict, List, Optional, Any
 from typing import Dict, List, Optional, Any
 from datetime import datetime
 from datetime import datetime
@@ -29,6 +30,8 @@ from datetime import datetime
 from .models import Trace, Message
 from .models import Trace, Message
 from .goal_models import GoalTree, Goal, GoalStats
 from .goal_models import GoalTree, Goal, GoalStats
 
 
+logger = logging.getLogger(__name__)
+
 
 
 class FileSystemTraceStore:
 class FileSystemTraceStore:
     """文件系统 Trace 存储"""
     """文件系统 Trace 存储"""
@@ -78,7 +81,7 @@ class FileSystemTraceStore:
 
 
         # 写入 meta.json
         # 写入 meta.json
         meta_file = self._get_meta_file(trace.trace_id)
         meta_file = self._get_meta_file(trace.trace_id)
-        meta_file.write_text(json.dumps(trace.to_dict(), indent=2, ensure_ascii=False))
+        meta_file.write_text(json.dumps(trace.to_dict(), indent=2, ensure_ascii=False), encoding="utf-8")
 
 
         # 创建空的 events.jsonl
         # 创建空的 events.jsonl
         events_file = self._get_events_file(trace.trace_id)
         events_file = self._get_events_file(trace.trace_id)
@@ -92,7 +95,7 @@ class FileSystemTraceStore:
         if not meta_file.exists():
         if not meta_file.exists():
             return None
             return None
 
 
-        data = json.loads(meta_file.read_text())
+        data = json.loads(meta_file.read_text(encoding="utf-8"))
 
 
         # 解析 datetime 字段
         # 解析 datetime 字段
         if data.get("created_at"):
         if data.get("created_at"):
@@ -100,7 +103,7 @@ class FileSystemTraceStore:
         if data.get("completed_at"):
         if data.get("completed_at"):
             data["completed_at"] = datetime.fromisoformat(data["completed_at"])
             data["completed_at"] = datetime.fromisoformat(data["completed_at"])
 
 
-        return Trace(**data)
+        return Trace.from_dict(data)
 
 
     async def update_trace(self, trace_id: str, **updates) -> None:
     async def update_trace(self, trace_id: str, **updates) -> None:
         """更新 Trace"""
         """更新 Trace"""
@@ -115,7 +118,7 @@ class FileSystemTraceStore:
 
 
         # 写回文件
         # 写回文件
         meta_file = self._get_meta_file(trace_id)
         meta_file = self._get_meta_file(trace_id)
-        meta_file.write_text(json.dumps(trace.to_dict(), indent=2, ensure_ascii=False))
+        meta_file.write_text(json.dumps(trace.to_dict(), indent=2, ensure_ascii=False), encoding="utf-8")
 
 
     async def list_traces(
     async def list_traces(
         self,
         self,
@@ -140,7 +143,7 @@ class FileSystemTraceStore:
                 continue
                 continue
 
 
             try:
             try:
-                data = json.loads(meta_file.read_text())
+                data = json.loads(meta_file.read_text(encoding="utf-8"))
 
 
                 # 过滤
                 # 过滤
                 if mode and data.get("mode") != mode:
                 if mode and data.get("mode") != mode:
@@ -158,7 +161,7 @@ class FileSystemTraceStore:
                 if data.get("completed_at"):
                 if data.get("completed_at"):
                     data["completed_at"] = datetime.fromisoformat(data["completed_at"])
                     data["completed_at"] = datetime.fromisoformat(data["completed_at"])
 
 
-                traces.append(Trace(**data))
+                traces.append(Trace.from_dict(data))
             except Exception:
             except Exception:
                 continue
                 continue
 
 
@@ -176,7 +179,7 @@ class FileSystemTraceStore:
             return None
             return None
 
 
         try:
         try:
-            data = json.loads(goal_file.read_text())
+            data = json.loads(goal_file.read_text(encoding="utf-8"))
             return GoalTree.from_dict(data)
             return GoalTree.from_dict(data)
         except Exception:
         except Exception:
             return None
             return None
@@ -184,7 +187,7 @@ class FileSystemTraceStore:
     async def update_goal_tree(self, trace_id: str, tree: GoalTree) -> None:
     async def update_goal_tree(self, trace_id: str, tree: GoalTree) -> None:
         """更新完整 GoalTree"""
         """更新完整 GoalTree"""
         goal_file = self._get_goal_file(trace_id)
         goal_file = self._get_goal_file(trace_id)
-        goal_file.write_text(json.dumps(tree.to_dict(), indent=2, ensure_ascii=False))
+        goal_file.write_text(json.dumps(tree.to_dict(), indent=2, ensure_ascii=False), encoding="utf-8")
 
 
     async def add_goal(self, trace_id: str, goal: Goal) -> None:
     async def add_goal(self, trace_id: str, goal: Goal) -> None:
         """添加 Goal 到 GoalTree"""
         """添加 Goal 到 GoalTree"""
@@ -201,7 +204,14 @@ class FileSystemTraceStore:
             "parent_id": goal.parent_id
             "parent_id": goal.parent_id
         }
         }
         await self.append_event(trace_id, "goal_added", event_data)
         await self.append_event(trace_id, "goal_added", event_data)
-        print(f"[DEBUG] Pushed goal_added event: goal_id={goal.id}, parent_id={goal.parent_id}")
+
+        # 打印详细的 goal 信息
+        desc_preview = goal.description[:80] + "..." if len(goal.description) > 80 else goal.description
+        print(f"[Goal Added] ID={goal.id}, Parent={goal.parent_id or 'root'}")
+        print(f"  📝 {desc_preview}")
+        if goal.reason:
+            reason_preview = goal.reason[:60] + "..." if len(goal.reason) > 60 else goal.reason
+            print(f"  💡 {reason_preview}")
 
 
     async def update_goal(self, trace_id: str, goal_id: str, **updates) -> None:
     async def update_goal(self, trace_id: str, goal_id: str, **updates) -> None:
         """更新 Goal 字段"""
         """更新 Goal 字段"""
@@ -363,10 +373,22 @@ class FileSystemTraceStore:
 
 
         # 4. 追加 message_added 事件
         # 4. 追加 message_added 事件
         affected_goals = await self._get_affected_goals(trace_id, message)
         affected_goals = await self._get_affected_goals(trace_id, message)
-        await self.append_event(trace_id, "message_added", {
+        event_id = await self.append_event(trace_id, "message_added", {
             "message": message.to_dict(),
             "message": message.to_dict(),
             "affected_goals": affected_goals
             "affected_goals": affected_goals
         })
         })
+        if event_id:
+            try:
+                from . import websocket as trace_ws
+
+                await trace_ws.broadcast_message_added(
+                    trace_id=trace_id,
+                    event_id=event_id,
+                    message_dict=message.to_dict(),
+                    affected_goals=affected_goals,
+                )
+            except Exception:
+                logger.exception("Failed to broadcast message_added (trace_id=%s, event_id=%s)", trace_id, event_id)
 
 
         return message.message_id
         return message.message_id
 
 
@@ -463,7 +485,7 @@ class FileSystemTraceStore:
             message_file = trace_dir / "messages" / f"{message_id}.json"
             message_file = trace_dir / "messages" / f"{message_id}.json"
             if message_file.exists():
             if message_file.exists():
                 try:
                 try:
-                    data = json.loads(message_file.read_text())
+                    data = json.loads(message_file.read_text(encoding="utf-8"))
                     return Message.from_dict(data)
                     return Message.from_dict(data)
                 except Exception:
                 except Exception:
                     pass
                     pass
@@ -483,7 +505,7 @@ class FileSystemTraceStore:
         messages = []
         messages = []
         for message_file in messages_dir.glob("*.json"):
         for message_file in messages_dir.glob("*.json"):
             try:
             try:
-                data = json.loads(message_file.read_text())
+                data = json.loads(message_file.read_text(encoding="utf-8"))
                 msg = Message.from_dict(data)
                 msg = Message.from_dict(data)
                 messages.append(msg)
                 messages.append(msg)
             except Exception:
             except Exception:
@@ -499,10 +521,14 @@ class FileSystemTraceStore:
         head_sequence: int
         head_sequence: int
     ) -> List[Message]:
     ) -> List[Message]:
         """
         """
-        获取主路径上的消息(从 head_sequence 沿 parent_sequence 链回溯到 root)
+        获取从 head_sequence 沿 parent_sequence 链回溯到 root 的完整路径
+
+        此函数是通用的路径追溯函数,返回从指定 head 到 root 的完整消息链。
+        只要 trace.head_sequence 管理正确(指向主路径),此函数自然返回主路径消息。
+        侧分支消息通过 parent_sequence 链自然被跳过(因为主路径的 parent 不指向侧分支)。
 
 
         Returns:
         Returns:
-            按 sequence 正序排列的主路径 Message 列表
+            按 sequence 正序排列的路径 Message 列表
         """
         """
         # 加载所有消息,建立 sequence -> Message 索引
         # 加载所有消息,建立 sequence -> Message 索引
         all_messages = await self.get_trace_messages(trace_id)
         all_messages = await self.get_trace_messages(trace_id)
@@ -545,7 +571,7 @@ class FileSystemTraceStore:
         # 确定文件路径
         # 确定文件路径
         messages_dir = self._get_messages_dir(message.trace_id)
         messages_dir = self._get_messages_dir(message.trace_id)
         message_file = messages_dir / f"{message_id}.json"
         message_file = messages_dir / f"{message_id}.json"
-        message_file.write_text(json.dumps(message.to_dict(), indent=2, ensure_ascii=False))
+        message_file.write_text(json.dumps(message.to_dict(), indent=2, ensure_ascii=False), encoding="utf-8")
 
 
     async def abandon_messages_after(self, trace_id: str, cutoff_sequence: int) -> List[str]:
     async def abandon_messages_after(self, trace_id: str, cutoff_sequence: int) -> List[str]:
         """
         """
@@ -600,7 +626,7 @@ class FileSystemTraceStore:
 
 
         # 读取现有数据
         # 读取现有数据
         if usage_file.exists():
         if usage_file.exists():
-            data = json.loads(usage_file.read_text())
+            data = json.loads(usage_file.read_text(encoding="utf-8"))
         else:
         else:
             data = {
             data = {
                 "summary": {
                 "summary": {
@@ -666,7 +692,7 @@ class FileSystemTraceStore:
         data["timeline"].append(timeline_entry)
         data["timeline"].append(timeline_entry)
 
 
         # 写回文件
         # 写回文件
-        usage_file.write_text(json.dumps(data, indent=2, ensure_ascii=False))
+        usage_file.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
 
 
     # ===== 事件流操作(用于 WebSocket 断线续传)=====
     # ===== 事件流操作(用于 WebSocket 断线续传)=====
 
 
@@ -681,7 +707,7 @@ class FileSystemTraceStore:
             return []
             return []
 
 
         events = []
         events = []
-        with events_file.open('r') as f:
+        with events_file.open('r', encoding='utf-8') as f:
             for line in f:
             for line in f:
                 try:
                 try:
                     event = json.loads(line.strip())
                     event = json.loads(line.strip())

+ 92 - 4
agent/trace/websocket.py

@@ -6,6 +6,7 @@ Trace WebSocket 推送
 
 
 from typing import Dict, Set, Any
 from typing import Dict, Set, Any
 from datetime import datetime
 from datetime import datetime
+import asyncio
 from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query
 from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query
 
 
 from .protocols import TraceStore
 from .protocols import TraceStore
@@ -90,16 +91,23 @@ async def watch_trace(
             if t.parent_trace_id == trace_id:
             if t.parent_trace_id == trace_id:
                 sub_traces[t.trace_id] = t.to_dict()
                 sub_traces[t.trace_id] = t.to_dict()
 
 
-        # 发送连接成功消息 + 完整状态
+        # 发送连接成功消息 + 完整状态(含 trace 当前执行状态)
+        from .run_api import _running_tasks  # 避免循环导入,在函数内 import
+        is_running = (
+            trace_id in _running_tasks and not _running_tasks[trace_id].done()
+        )
         await websocket.send_json({
         await websocket.send_json({
             "event": "connected",
             "event": "connected",
             "trace_id": trace_id,
             "trace_id": trace_id,
             "current_event_id": trace.last_event_id,
             "current_event_id": trace.last_event_id,
+            "trace_status": trace.status if not is_running else "running",
+            "is_running": is_running,
             "goal_tree": goal_tree.to_dict() if goal_tree else None,
             "goal_tree": goal_tree.to_dict() if goal_tree else None,
             "sub_traces": sub_traces
             "sub_traces": sub_traces
         })
         })
 
 
         # 补发历史事件(since_event_id=0 表示补发所有历史)
         # 补发历史事件(since_event_id=0 表示补发所有历史)
+        last_sent_event_id = since_event_id
         if since_event_id >= 0:
         if since_event_id >= 0:
             missed_events = await store.get_events(trace_id, since_event_id)
             missed_events = await store.get_events(trace_id, since_event_id)
             # 限制补发数量(最多 100 条)
             # 限制补发数量(最多 100 条)
@@ -111,16 +119,36 @@ async def watch_trace(
             else:
             else:
                 for evt in missed_events:
                 for evt in missed_events:
                     await websocket.send_json(evt)
                     await websocket.send_json(evt)
+                    if isinstance(evt, dict) and isinstance(evt.get("event_id"), int):
+                        last_sent_event_id = max(last_sent_event_id, evt["event_id"])
 
 
-        # 保持连接(等待客户端断开或接收消息)
+        # 保持连接:同时支持心跳 + 轮询 events.jsonl(跨进程写入时也能实时推送
         while True:
         while True:
             try:
             try:
-                # 接收客户端消息(心跳检测)
-                data = await websocket.receive_text()
+                # 允许在没有客户端消息时继续轮询事件流
+                data = await asyncio.wait_for(websocket.receive_text(), timeout=0.5)
                 if data == "ping":
                 if data == "ping":
                     await websocket.send_json({"event": "pong"})
                     await websocket.send_json({"event": "pong"})
             except WebSocketDisconnect:
             except WebSocketDisconnect:
                 break
                 break
+            except asyncio.TimeoutError:
+                pass
+
+            try:
+                new_events = await store.get_events(trace_id, last_sent_event_id)
+                if len(new_events) > 100:
+                    await websocket.send_json({
+                        "event": "error",
+                        "message": f"Too many missed events ({len(new_events)}), please reload via REST API"
+                    })
+                    continue
+
+                for evt in new_events:
+                    await websocket.send_json(evt)
+                    if isinstance(evt, dict) and isinstance(evt.get("event_id"), int):
+                        last_sent_event_id = max(last_sent_event_id, evt["event_id"])
+            except WebSocketDisconnect:
+                break
 
 
     finally:
     finally:
         # 清理连接
         # 清理连接
@@ -196,6 +224,33 @@ async def broadcast_goal_updated(
     await _broadcast_to_trace(trace_id, message)
     await _broadcast_to_trace(trace_id, message)
 
 
 
 
+async def broadcast_message_added(
+    trace_id: str,
+    event_id: int,
+    message_dict: Dict[str, Any],
+    affected_goals: list[Dict[str, Any]] = None,
+):
+    """
+    广播 Message 添加事件(不在此处写入 events.jsonl)
+
+    说明:
+    - message_added 的 events.jsonl 写入由 TraceStore.append_event 负责
+    - 这里仅负责把“已经持久化”的事件推送给当前活跃连接
+    """
+    if trace_id not in _active_connections:
+        return
+
+    message = {
+        "event": "message_added",
+        "event_id": event_id,
+        "ts": datetime.now().isoformat(),
+        "message": message_dict,
+        "affected_goals": affected_goals or [],
+    }
+
+    await _broadcast_to_trace(trace_id, message)
+
+
 async def broadcast_sub_trace_started(
 async def broadcast_sub_trace_started(
     trace_id: str,
     trace_id: str,
     sub_trace_id: str,
     sub_trace_id: str,
@@ -310,6 +365,39 @@ async def broadcast_trace_completed(trace_id: str, total_messages: int):
         del _active_connections[trace_id]
         del _active_connections[trace_id]
 
 
 
 
+async def broadcast_trace_status_changed(trace_id: str, status: str):
+    """
+    广播 Trace 状态变化事件(用于暂停/继续等状态切换)
+
+    Args:
+        trace_id: Trace ID
+        status: 新状态 (running/stopped/completed/failed)
+    """
+    if trace_id not in _active_connections:
+        return
+
+    store = get_trace_store()
+    trace = await store.get_trace(trace_id)
+    if not trace:
+        return
+
+    event_id = await store.append_event(trace_id, "trace_status_changed", {
+        "status": status
+    })
+
+    message = {
+        "event": "trace_status_changed",
+        "event_id": event_id,
+        "ts": datetime.now().isoformat(),
+        "trace_id": trace_id,
+        "status": status,
+        "total_cost": trace.total_cost,
+        "total_messages": trace.total_messages
+    }
+
+    await _broadcast_to_trace(trace_id, message)
+
+
 # ===== 内部辅助函数 =====
 # ===== 内部辅助函数 =====
 
 
 
 

+ 7 - 0
agent/utils/__init__.py

@@ -0,0 +1,7 @@
+"""
+工具函数模块
+"""
+
+from agent.utils.logging import setup_logging
+
+__all__ = ["setup_logging"]

+ 40 - 0
agent/utils/logging.py

@@ -0,0 +1,40 @@
+"""
+日志配置工具
+
+提供统一的日志配置方法。
+"""
+
+import logging
+from typing import Optional
+
+
+def setup_logging(
+    level: str = "INFO",
+    format: Optional[str] = None,
+    file: Optional[str] = None
+):
+    """
+    配置日志系统
+
+    Args:
+        level: 日志级别(DEBUG, INFO, WARNING, ERROR, CRITICAL)
+        format: 日志格式(None 使用默认格式)
+        file: 日志文件路径(None 只输出到控制台)
+    """
+    log_level = getattr(logging, level.upper(), logging.INFO)
+    log_format = format or "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
+
+    handlers = [logging.StreamHandler()]
+    if file:
+        handlers.append(logging.FileHandler(file, encoding="utf-8"))
+
+    logging.basicConfig(
+        level=log_level,
+        format=log_format,
+        handlers=handlers,
+        force=True
+    )
+
+    # 设置第三方库日志级别
+    logging.getLogger("httpx").setLevel(logging.WARNING)
+    logging.getLogger("httpcore").setLevel(logging.WARNING)

+ 18 - 1
api_server.py

@@ -19,6 +19,8 @@ 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.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
 from agent.trace.websocket import router as ws_router, set_trace_store as set_ws_trace_store
 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
 
 
 
 
 # ===== 日志配置 =====
 # ===== 日志配置 =====
@@ -29,6 +31,9 @@ logging.basicConfig(
 )
 )
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
+# 设置WebSocket日志推送
+setup_websocket_logging(level=logging.INFO)
+
 
 
 # ===== FastAPI 应用 =====
 # ===== FastAPI 应用 =====
 
 
@@ -68,13 +73,15 @@ from agent.llm import create_openrouter_llm_call
 runner = AgentRunner(
 runner = AgentRunner(
     trace_store=trace_store,
     trace_store=trace_store,
     llm_call=create_openrouter_llm_call(model="anthropic/claude-sonnet-4.5"),
     llm_call=create_openrouter_llm_call(model="anthropic/claude-sonnet-4.5"),
-    experiences_path="./.cache/experiences.md",  # 经验文件路径
 )
 )
 set_runner(runner)
 set_runner(runner)
 
 
 
 
 # ===== 注册路由 =====
 # ===== 注册路由 =====
 
 
+# Examples API(GET /api/examples)
+app.include_router(examples_router)
+
 # Trace 执行 API(POST + GET /running,需配置 Runner)
 # Trace 执行 API(POST + GET /running,需配置 Runner)
 # 注意:run_router 必须在 api_router 之前注册,否则 GET /running 会被 /{trace_id} 捕获
 # 注意:run_router 必须在 api_router 之前注册,否则 GET /running 会被 /{trace_id} 捕获
 app.include_router(run_router)
 app.include_router(run_router)
@@ -88,6 +95,16 @@ app.include_router(api_router)
 # Trace WebSocket(实时推送)
 # Trace WebSocket(实时推送)
 app.include_router(ws_router)
 app.include_router(ws_router)
 
 
+# Logs WebSocket(日志推送)
+app.include_router(logs_router)
+
+
+@app.on_event("startup")
+async def on_startup():
+    """服务器启动时执行状态对齐"""
+    from agent.trace.run_api import reconcile_traces
+    await reconcile_traces()
+
 @app.websocket("/ws_ping")
 @app.websocket("/ws_ping")
 async def ws_ping(websocket: WebSocket):
 async def ws_ping(websocket: WebSocket):
     await websocket.accept()
     await websocket.accept()

+ 28 - 26
config/feishu_contacts.json

@@ -1,28 +1,30 @@
 [
 [
-    {
-        "name": "谭景玉",
-        "description": "",
-        "open_id": "ou_11fdbd559cc6513ab53ff06d6c63413d",
-        "chat_id": "oc_56e85f0e2c97405d176729b62d8f56e5"
-    },
-    {
-        "name": "王华东",
-        "description": "",
-        "open_id": "ou_82340312cf9d215f49a41b67fa9c02c2"
-    },
-    {
-        "name": "孙若天",
-        "description": "",
-        "open_id": "ou_ede69f28c2617bf80a7574f059879c8d"
-    },
-    {
-        "name": "刘斌",
-        "description": "",
-        "open_id": "ou_50c2307c3531e6293b3d5533d14592e9"
-    },
-    {
-        "name": "关涛",
-        "description": "",
-        "open_id": "ou_90b80ed994fe41b7f038a63cb9182f72"
-    }
+  {
+    "name": "谭景玉",
+    "description": "",
+    "open_id": "ou_11fdbd559cc6513ab53ff06d6c63413d",
+    "chat_id": "oc_56e85f0e2c97405d176729b62d8f56e5"
+  },
+  {
+    "name": "王华东",
+    "description": "",
+    "open_id": "ou_82340312cf9d215f49a41b67fa9c02c2"
+  },
+  {
+    "name": "孙若天",
+    "description": "",
+    "open_id": "ou_ede69f28c2617bf80a7574f059879c8d",
+    "chat_id": "oc_98019f9a0419b46a215ca604b04c5cc6"
+  },
+  {
+    "name": "刘斌",
+    "description": "",
+    "open_id": "ou_50c2307c3531e6293b3d5533d14592e9"
+  },
+  {
+    "name": "关涛",
+    "description": "",
+    "open_id": "ou_90b80ed994fe41b7f038a63cb9182f72",
+    "chat_id": "oc_ac9633d2c61f43b5049c425305482491"
+  }
 ]
 ]

+ 75 - 969
docs/README.md

@@ -1,1016 +1,122 @@
-# Agent 功能需求与架构设计文档
+# Agent 系统文档
 
 
-## 文档维护规范
+## 文档导航
 
 
-0. **先改文档,再动代码** - 新功能或重大修改需先完成文档更新、并完成审阅后,再进行代码实现;除非改动较小、不被文档涵盖
-1. **文档分层,链接代码** - 重要或复杂设计可以另有详细文档;关键实现需标注代码文件路径;格式:`module/file.py:function_name`
-2. **简洁快照,日志分离** - 只记录最重要的、与代码准确对应的或者明确的已完成的设计的信息,避免推测、建议,或大量代码;决策依据或修改日志若有必要,可在`docs/decisions.md`另行记录
+本文档是项目总览和文档导航。详细的模块文档请参考:
 
 
----
+### 核心模块
 
 
-## 系统概览
+- **[Agent Core 模块](../agent/README.md)** - Agent 核心引擎、工具系统、记忆管理
+  - [架构设计](../agent/docs/architecture.md) - Agent 框架完整架构
+  - [工具系统](../agent/docs/tools.md)
+  - [Skills 指南](../agent/docs/skills.md)
+  - [Trace API](../agent/docs/trace-api.md)
+  - [多模态支持](../agent/docs/multimodal.md)
+  - [设计决策](../agent/docs/decisions.md)
 
 
-**核心理念:所有 Agent 都是 Trace**
+- **[Gateway 模块](../gateway/README.md)** - Agent 注册、消息路由、在线状态管理
+  - [架构设计](../gateway/docs/architecture.md)
+  - [部署指南](../gateway/docs/deployment.md)
+  - [API 参考](../gateway/docs/api.md)
+  - [设计决策](../gateway/docs/decisions.md)
+  - [Enterprise 层](../gateway/docs/enterprise/overview.md)
+  - [A2A IM 使用](../gateway/client/a2a_im.md) - Agent 间通讯工具
 
 
-| 类型 | 创建方式 | 父子关系 | 状态 |
-|------|---------|---------|------|
-| 主 Agent | 直接调用 `runner.run()` | 无 parent | 正常执行 |
-| 子 Agent | 通过 `agent` 工具 | `parent_trace_id` / `parent_goal_id` 指向父 | 正常执行 |
-| 人类协助 | 通过 `ask_human` 工具 | `parent_trace_id` 指向父 | 阻塞等待 |
+### 跨模块文档
 
 
----
-
-## 核心架构
-
-### 模块结构
-
-```
-agent/
-├── core/                  # 核心引擎
-│   ├── runner.py          # AgentRunner + 运行时配置
-│   └── presets.py         # Agent 预设(explore、analyst 等)
-│
-├── trace/                 # 执行追踪(含计划管理)
-│   ├── models.py          # Trace, Message
-│   ├── goal_models.py     # Goal, GoalTree, GoalStats
-│   ├── protocols.py       # TraceStore 接口
-│   ├── store.py           # FileSystemTraceStore 实现
-│   ├── goal_tool.py       # goal 工具(计划管理)
-│   ├── compaction.py      # Context 压缩
-│   ├── api.py             # REST API
-│   ├── websocket.py       # WebSocket API
-│   └── trace_id.py        # Trace ID 生成工具
-│
-├── tools/                 # 外部交互工具
-│   ├── registry.py        # 工具注册表
-│   ├── schema.py          # Schema 生成器
-│   ├── models.py          # ToolResult, ToolContext
-│   └── builtin/
-│       ├── file/          # 文件操作(read, write, edit, glob, grep)
-│       ├── browser/       # 浏览器自动化
-│       ├── bash.py        # 命令执行
-│       ├── sandbox.py     # 沙箱环境
-│       ├── search.py      # 网络搜索
-│       ├── webfetch.py    # 网页抓取
-│       ├── skill.py       # 技能加载
-│       └── subagent.py    # agent / evaluate 工具(子 Agent 创建与评估)
-│
-├── memory/                # 跨会话记忆
-│   ├── models.py          # Experience, Skill
-│   ├── protocols.py       # MemoryStore 接口
-│   ├── stores.py          # 存储实现
-│   ├── skill_loader.py    # Skill 加载器
-│   └── skills/            # 内置 Skills(自动注入 system prompt)
-│       ├── planning.md    # 计划与 Goal 工具使用
-│       ├── research.md    # 搜索与内容研究
-│       └── browser.md     # 浏览器自动化
-│
-├── llm/                   # LLM 集成
-│   ├── gemini.py          # Gemini Provider
-│   ├── openrouter.py      # OpenRouter Provider(OpenAI 兼容格式)
-│   ├── yescode.py         # Yescode Provider(Anthropic 原生 Messages API)
-│   └── prompts/           # Prompt 工具
-```
+- [A2A IM 系统](./a2a-im.md) - Agent 间即时通讯系统架构
+- [知识管理](../knowhub/docs/knowledge-management.md) - 知识结构、API、集成方式
+- [Context 管理](./context-management.md) - Goals、压缩、Plan 注入策略
 
 
-### 职责划分
+### 研究文档
 
 
-| 模块 | 职责 |
-|-----|------|
-| **core/** | Agent 执行引擎 + 预设配置 |
-| **trace/** | 执行追踪 + 计划管理 |
-| **tools/** | 与外部世界交互(文件、命令、网络、浏览器) |
-| **memory/** | 跨会话知识(Skills、Experiences) |
-| **llm/** | LLM Provider 适配 |
+- [A2A 协议调研](./research/a2a-protocols.md) - 行业 A2A 通信协议和框架对比
+- [A2A 跨设备通信](./research/a2a-cross-device.md) - 跨设备 Agent 通信方案(内部)
+- [A2A Trace 存储](./research/a2a-trace-storage.md) - 跨设备 Trace 存储方案详细设计
+- [MAMP 协议](./research/a2a-mamp-protocol.md) - 与外部 Agent 系统的通用交互协议
 
 
-### 三层记忆模型
-
-```
-┌─────────────────────────────────────────────────────────────┐
-│ Layer 3: Skills(技能库)                                     │
-│ - Markdown 文件,存储领域知识和能力描述                        │
-│ - 通过 skill 工具按需加载到对话历史                            │
-└─────────────────────────────────────────────────────────────┘
-                              ▲
-                              │ 归纳
-┌─────────────────────────────────────────────────────────────┐
-│ Layer 2: Experience(经验库)                                 │
-│ - 数据库存储,条件 + 规则 + 证据                              │
-│ - 向量检索,注入到 system prompt                              │
-└─────────────────────────────────────────────────────────────┘
-                              ▲
-                              │ 提取
-┌─────────────────────────────────────────────────────────────┐
-│ Layer 1: Trace(任务状态)                                    │
-│ - 当前任务的工作记忆                                          │
-│ - Trace + Messages 记录执行过程                               │
-│ - Goals 管理执行计划                                          │
-└─────────────────────────────────────────────────────────────┘
-```
-
-### LLM Provider 适配
-
-#### 内部格式
-
-框架内部统一使用 OpenAI 兼容格式(`List[Dict]`)存储和传递消息。各 Provider 负责双向转换:
-
-| 方向 | 说明 |
-|------|------|
-| 入(LLM 响应 → 框架) | 提取 content、tool_calls、usage,转换为统一 Dict |
-| 出(框架 → LLM 请求) | OpenAI 格式消息列表 → 各 API 原生格式 |
-
-#### 工具消息分组
-
-存储层每个 tool result 独立一条 Message(OpenAI 格式最大公约数)。各 Provider 在出方向按 API 要求自行分组:
-
-| Provider | 分组方式 |
-|----------|---------|
-| OpenRouter | 无需分组(OpenAI 原生支持独立 tool 消息) |
-| Yescode | `_convert_messages_to_anthropic` 合并连续 tool 消息为单个 user message |
-| Gemini | `_convert_messages_to_gemini` 通过 buffer 合并连续 tool 消息 |
-
-#### 跨 Provider 续跑:tool_call_id 规范化
-
-不同 Provider 生成的 tool_call_id 格式不同(OpenAI: `call_xxx`,Anthropic: `toolu_xxx`,Gemini: 合成 `call_0`)。存储层按原样保存,不做规范化。
+---
 
 
-跨 Provider 续跑时,出方向转换前检测历史中的 tool_call_id 格式,不兼容时统一重写为目标格式(保持 tool_use / tool_result 配对一致)。同格式跳过,零开销。Gemini 按 function name 匹配,无需重写。
+## 文档维护规范
 
 
-**实现**:`agent/llm/openrouter.py:_normalize_tool_call_ids`, `agent/llm/yescode.py:_normalize_tool_call_ids`
+0. **先改文档,再动代码** - 新功能或重大修改需先完成文档更新、并完成审阅后,再进行代码实现;除非改动较小、不被文档涵盖
+1. **文档分层,链接代码** - 重要或复杂设计可以另有详细文档;关键实现需标注代码文件路径;格式:`module/file.py:function_name`
+2. **简洁快照,日志分离** - 只记录最重要的、与代码准确对应的或者明确的已完成的设计的信息,避免推测、建议,或大量代码;决策依据或修改日志若有必要,可在模块的 decisions.md 另行记录
 
 
 ---
 ---
 
 
-## 核心流程:Agent Loop
+## 项目概览
 
 
-### 参数分层
+### 系统架构
 
 
 ```
 ```
-Layer 1: Infrastructure(基础设施,AgentRunner 构造时设置)
-  trace_store, memory_store, tool_registry, llm_call, skills_dir, utility_llm_call
-
-Layer 2: RunConfig(运行参数,每次 run 时指定)
-  ├─ 模型层:model, temperature, max_iterations, tools
-  └─ 框架层:trace_id, agent_type, uid, system_prompt, parent_trace_id, ...
-
-Layer 3: Messages(任务消息,OpenAI SDK 格式 List[Dict])
-  [{"role": "user", "content": "分析这张图的构图"}]
+Agent 系统
+├── agent/          # Agent Core - 核心引擎、工具、记忆
+├── gateway/        # Gateway - 消息路由、Agent 注册
+├── docs/           # 跨模块文档
+└── examples/       # 使用示例和集成示例
 ```
 ```
 
 
-### RunConfig
+### 核心理念
 
 
-```python
-@dataclass
-class RunConfig:
-    # 模型层参数
-    model: str = "gpt-4o"
-    temperature: float = 0.3
-    max_iterations: int = 200
-    tools: Optional[List[str]] = None          # None = 全部已注册工具
-
-    # 框架层参数
-    agent_type: str = "default"
-    uid: Optional[str] = None
-    system_prompt: Optional[str] = None        # None = 从 skills 自动构建
-    skills: Optional[List[str]] = None         # 注入 system prompt 的 skill 名称列表;None = 按 preset 决定
-    enable_memory: bool = True
-    auto_execute_tools: bool = True
-    name: Optional[str] = None                 # 显示名称(空则由 utility_llm 自动生成)
-
-    # Trace 控制
-    trace_id: Optional[str] = None             # None = 新建
-    parent_trace_id: Optional[str] = None      # 子 Agent 专用
-    parent_goal_id: Optional[str] = None
-
-    # 续跑控制
-    after_sequence: Optional[int] = None       # 从哪条消息后续跑(message sequence)
-```
+**所有 Agent 都是 Trace**
 
 
-**实现**:`agent/core/runner.py:RunConfig`
+| 类型 | 创建方式 | 父子关系 | 状态 |
+|------|---------|---------|------|
+| 主 Agent | 直接调用 `runner.run()` | 无 parent | 正常执行 |
+| 子 Agent | 通过 `agent` 工具 | `parent_trace_id` / `parent_goal_id` 指向父 | 正常执行 |
+| 人类协助 | 通过 `ask_human` 工具 | `parent_trace_id` 指向父 | 阻塞等待 |
 
 
-### 三种运行模式
+### 模块职责
 
 
-通过 RunConfig 参数自然区分,统一入口 `run(messages, config)`:
+| 模块 | 职责 | 详细文档 |
+|-----|------|---------|
+| **agent/core/** | Agent 执行引擎 + 预设配置 | [架构设计](../agent/docs/architecture.md) |
+| **agent/trace/** | 执行追踪 + 计划管理 | [Trace API](../agent/docs/trace-api.md) |
+| **agent/tools/** | 与外部世界交互 | [工具系统](../agent/docs/tools.md) |
+| **agent/skill/** | 技能系统 | [Skills 指南](../agent/docs/skills.md) |
+| **agent/llm/** | LLM Provider 适配 | [架构设计](../agent/docs/architecture.md#llm-provider-适配) |
+| **gateway/core/** | Agent 注册和消息路由 | [Gateway 架构](../gateway/docs/architecture.md) |
+| **gateway/client/** | Gateway 客户端 SDK | [A2A IM](../gateway/client/a2a_im.md) |
 
 
-| 模式 | trace_id | after_sequence | messages 含义 | API 端点 |
-|------|----------|---------------|--------------|----------|
-| 新建 | None | - | 初始任务消息 | `POST /api/traces` |
-| 续跑 | 已有 ID | None 或 == head | 追加到末尾的新消息 | `POST /api/traces/{id}/run` |
-| 回溯 | 已有 ID | 主路径上 < head | 在插入点之后追加的新消息 | `POST /api/traces/{id}/run` |
+---
 
 
-Runner 根据 `after_sequence` 与当前 `head_sequence` 的关系自动判断行为,前端无需指定模式。
+## 快速开始
 
 
-### 执行流程
+### Agent Core
 
 
 ```python
 ```python
-async def run(messages: List[Dict], config: RunConfig = None) -> AsyncIterator[Union[Trace, Message]]:
-    # Phase 1: PREPARE TRACE
-    #   无 trace_id → 创建新 Trace(生成 name,初始化 GoalTree)
-    #   有 trace_id + after_sequence 为 None 或 == head → 加载已有 Trace,状态置为 running
-    #   有 trace_id + after_sequence < head → 加载 Trace,执行 rewind(快照 GoalTree,重建,设 parent_sequence)
-    trace = await _prepare_trace(config)
-    yield trace
-
-    # Phase 2: BUILD HISTORY
-    #   从 head_sequence 沿 parent chain 回溯构建主路径消息
-    #   构建 system prompt(新建时注入 skills/experiences;续跑时复用已有)
-    #   追加 input messages(设置 parent_sequence 指向当前 head)
-    history, sequence = await _build_history(trace, messages, config)
-
-    # Phase 3: AGENT LOOP
-    for iteration in range(config.max_iterations):
-        # 周期性注入 GoalTree + Active Collaborators(每 10 轮)
-        if iteration % 10 == 0:
-            inject_context(goal_tree, collaborators)
-
-        response = await llm_call(messages=history, model=config.model, tools=tool_schemas)
-
-        # 按需自动创建 root goal(兜底)
-        # 记录 assistant Message
-        # 执行工具,记录 tool Messages
-        # 无 tool_calls 则 break
-
-    # Phase 4: COMPLETE
-    #   更新 Trace 状态 (completed/failed)
-    trace.status = "completed"
-    yield trace
-```
+from agent.core import AgentRunner, RunConfig
 
 
-**实现**:`agent/core/runner.py:AgentRunner`
+runner = AgentRunner(...)
 
 
-### 回溯(Rewind)
-
-回溯通过 `RunConfig(trace_id=..., after_sequence=N)` 触发(N 在主路径上且 < head_sequence),在 Phase 1 中执行:
-
-1. **验证插入点**:确保不截断在 assistant(tool_calls) 和 tool response 之间
-2. **快照 GoalTree**:将当前完整 GoalTree 存入 `events.jsonl`(rewind 事件的 `goal_tree_snapshot` 字段)
-3. **按时间重建 GoalTree**:以截断点消息的 `created_at` 为界,保留 `created_at <= cutoff_time` 的所有 goals(无论状态),丢弃 cutoff 之后创建的 goals,清空 `current_id`。将被保留的 `in_progress` goal 重置为 `pending`
-4. **设置 parent_sequence**:新消息的 `parent_sequence` 指向 rewind 点,旧消息自动脱离主路径
-5. **更新 Trace**:`head_sequence` 更新为新消息的 sequence,status 改回 running
-
-新消息的 sequence 从 `last_sequence + 1` 开始(全局递增,不复用)。旧消息无需标记 abandoned,通过消息树结构自然隔离。
-
-### 调用接口
-
-三种模式共享同一入口 `run(messages, config)`:
-
-```python
-# 新建
 async for item in runner.run(
 async for item in runner.run(
     messages=[{"role": "user", "content": "分析项目架构"}],
     messages=[{"role": "user", "content": "分析项目架构"}],
-    config=RunConfig(model="gpt-4o"),
+    config=RunConfig(model="gpt-4o")
 ):
 ):
-    ...
-
-# 续跑:在已有 trace 末尾追加消息继续执行
-async for item in runner.run(
-    messages=[{"role": "user", "content": "继续"}],
-    config=RunConfig(trace_id="existing-trace-id"),
-):
-    ...
-
-# 回溯:从指定 sequence 处切断,插入新消息重新执行
-# after_sequence=5 表示新消息的 parent_sequence=5,从此处开始
-async for item in runner.run(
-    messages=[{"role": "user", "content": "换一个方案试试"}],
-    config=RunConfig(trace_id="existing-trace-id", after_sequence=5),
-):
-    ...
-
-# 重新生成:回溯后不插入新消息,直接基于已有消息重跑
-async for item in runner.run(
-    messages=[],
-    config=RunConfig(trace_id="existing-trace-id", after_sequence=5),
-):
-    ...
+    print(item)
 ```
 ```
 
 
-`after_sequence` 的值是 message 的 `sequence` 号,可通过 `GET /api/traces/{trace_id}/messages` 查看。如果指定的 sequence 是一条带 `tool_calls` 的 assistant 消息,系统会自动将截断点扩展到其所有对应的 tool response 之后(安全截断)。
-
-**停止运行**:
-
-```python
-# 停止正在运行的 Trace
-await runner.stop(trace_id)
-```
-
-调用后 agent loop 在下一个检查点退出,Trace 状态置为 `stopped`,同时保存当前 `head_sequence`(确保续跑时能正确加载完整历史)。
-
-**消息完整性保护(orphaned tool_call 修复)**:续跑加载历史时,`_build_history` 自动检测并修复 orphaned tool_calls(`_heal_orphaned_tool_calls`)。当 agent 被 stop/crash 中断时,可能存在 assistant 的 tool_calls 没有对应的 tool results(包括部分完成的情况:3 个 tool_call 只有 1 个 tool_result)。直接发给 LLM 会导致 400 错误。
-
-修复策略:为每个缺失的 tool_result **插入合成的中断通知**(而非裁剪 assistant 消息):
-
-| 工具类型 | 合成 tool_result 内容 |
-|----------|---------------------|
-| 普通工具 | 简短中断提示,建议重新调用 |
-| agent/evaluate | 结构化中断信息,包含 `sub_trace_id`、执行统计、`continue_from` 用法指引 |
+详见:[Agent Core README](../agent/README.md)
 
 
-agent 工具的合成结果对齐正常返回值格式(含 `sub_trace_id` 字段),主 Agent 可直接使用 `agent(task=..., continue_from=sub_trace_id)` 续跑被中断的子 Agent。合成消息持久化存储,确保幂等。
-
-**实现**:`agent/core/runner.py:AgentRunner._heal_orphaned_tool_calls`
-
-- `run(messages, config)`:**核心方法**,流式返回 `AsyncIterator[Union[Trace, Message]]`
-- `run_result(messages, config, on_event=None)`:便利方法,内部消费 `run()`,返回结构化结果。`on_event` 回调可实时接收每个 Trace/Message 事件(用于调试时输出子 Agent 执行过程)。主要用于 `agent`/`evaluate` 工具内部
-
-### REST API
-
-#### 查询端点
-
-| 方法 | 路径 | 说明 |
-|------|------|------|
-| GET  | `/api/traces` | 列出 Traces |
-| GET  | `/api/traces/{id}` | 获取 Trace 详情(含 GoalTree、Sub-Traces) |
-| GET  | `/api/traces/{id}/messages` | 获取 Messages(支持 mode=main_path/all) |
-| GET  | `/api/traces/running` | 列出正在运行的 Trace |
-| WS   | `/api/traces/{id}/watch` | 实时事件推送 |
-
-**实现**:`agent/trace/api.py`, `agent/trace/websocket.py`
-
-#### 控制端点
-
-需在 `api_server.py` 中配置 Runner。执行在后台异步进行,通过 WebSocket 监听进度。
-
-| 方法 | 路径 | 说明 |
-|------|------|------|
-| POST | `/api/traces` | 新建 Trace 并执行 |
-| POST | `/api/traces/{id}/run` | 运行(统一续跑 + 回溯) |
-| POST | `/api/traces/{id}/stop` | 停止运行中的 Trace |
-| POST | `/api/traces/{id}/reflect` | 触发反思,从执行历史中提取经验 |
+### Gateway
 
 
 ```bash
 ```bash
-# 新建
-curl -X POST http://localhost:8000/api/traces \
-  -H "Content-Type: application/json" \
-  -d '{"messages": [{"role": "user", "content": "分析项目架构"}], "model": "gpt-4o"}'
-
-# 续跑(after_sequence 为 null 或省略)
-curl -X POST http://localhost:8000/api/traces/{trace_id}/run \
-  -d '{"messages": [{"role": "user", "content": "继续深入分析"}]}'
-
-# 回溯:从 sequence 5 处截断,插入新消息重新执行
-curl -X POST http://localhost:8000/api/traces/{trace_id}/run \
-  -d '{"after_sequence": 5, "messages": [{"role": "user", "content": "换一个方案"}]}'
-
-# 重新生成:回溯到 sequence 5,不插入新消息,直接重跑
-curl -X POST http://localhost:8000/api/traces/{trace_id}/run \
-  -d '{"after_sequence": 5, "messages": []}'
-
-# 停止
-curl -X POST http://localhost:8000/api/traces/{trace_id}/stop
-
-# 反思:追加反思 prompt 运行,结果追加到 experiences 文件
-curl -X POST http://localhost:8000/api/traces/{trace_id}/reflect \
-  -d '{"focus": "为什么第三步选择了错误的方案"}'
-```
-
-响应立即返回 `{"trace_id": "...", "status": "started"}`,通过 `WS /api/traces/{trace_id}/watch` 监听实时事件。
-
-**实现**:`agent/trace/run_api.py`
-
-#### 经验端点
-
-| 方法 | 路径 | 说明 |
-|------|------|------|
-| GET  | `/api/experiences` | 读取经验文件内容 |
-
-**实现**:`agent/trace/run_api.py`
-
----
-
-## 数据模型
-
-### Trace(任务执行)
-
-一次完整的 Agent 执行。所有 Agent(主、子、人类协助)都是 Trace。
-
-```python
-@dataclass
-class Trace:
-    trace_id: str
-    mode: Literal["call", "agent"]           # 单次调用 or Agent 模式
-
-    # Prompt 标识
-    prompt_name: Optional[str] = None
-
-    # Agent 模式特有
-    task: Optional[str] = None
-    agent_type: Optional[str] = None
-
-    # 父子关系(Sub-Trace 特有)
-    parent_trace_id: Optional[str] = None    # 父 Trace ID
-    parent_goal_id: Optional[str] = None     # 哪个 Goal 启动的
-
-    # 状态
-    status: Literal["running", "completed", "failed", "stopped"] = "running"
-
-    # 统计
-    total_messages: int = 0
-    total_tokens: int = 0                    # 总 tokens(prompt + completion)
-    total_prompt_tokens: int = 0
-    total_completion_tokens: int = 0
-    total_cost: float = 0.0
-    total_duration_ms: int = 0
-
-    # 进度追踪
-    last_sequence: int = 0                   # 最新 message 的 sequence(全局递增,不复用)
-    head_sequence: int = 0                   # 当前主路径的头节点 sequence(用于 build_llm_messages)
-    last_event_id: int = 0                   # 最新事件 ID(用于 WS 续传)
-
-    # 配置
-    uid: Optional[str] = None
-    model: Optional[str] = None              # 默认模型
-    tools: Optional[List[Dict]] = None       # 工具定义(OpenAI 格式)
-    llm_params: Dict[str, Any] = {}          # LLM 参数(temperature 等)
-    context: Dict[str, Any] = {}             # 元数据(含 collaborators 列表)
-
-    # 当前焦点
-    current_goal_id: Optional[str] = None
-
-    # 结果
-    result_summary: Optional[str] = None
-    error_message: Optional[str] = None
-
-    # 时间
-    created_at: datetime
-    completed_at: Optional[datetime] = None
-```
-
-**实现**:`agent/trace/models.py`
-
-### Goal(目标节点)
-
-计划中的一个目标,支持层级结构。单独存储于 `goal.json`。
-
-```python
-@dataclass
-class Goal:
-    id: str                                  # 内部 ID("1", "2"...)
-    description: str
-    reason: str = ""                         # 创建理由
-    parent_id: Optional[str] = None          # 父 Goal ID
-    type: GoalType = "normal"                # normal | agent_call
-    status: GoalStatus = "pending"           # pending | in_progress | completed | abandoned
-    summary: Optional[str] = None            # 完成/放弃时的总结
-
-    # agent_call 特有(启动 Sub-Trace)
-    sub_trace_ids: Optional[List[str]] = None
-    agent_call_mode: Optional[str] = None    # explore | delegate | evaluate
-    sub_trace_metadata: Optional[Dict] = None
-
-    # 统计
-    self_stats: GoalStats                    # 自身 Messages 统计
-    cumulative_stats: GoalStats              # 包含子孙的累计统计
-
-    created_at: datetime
-```
-
-**Goal 类型**:
-- `normal` - 普通目标,由 Agent 直接执行
-- `agent_call` - 通过 `agent`/`evaluate` 工具创建的目标,会启动 Sub-Trace
-
-**agent_call 类型的 Goal**:
-- 调用 `agent`/`evaluate` 工具时自动设置
-- `agent_call_mode` 记录使用的模式(explore/delegate/evaluate)
-- `sub_trace_ids` 记录创建的所有 Sub-Trace ID
-- 状态转换:pending → in_progress(Sub-Trace 启动)→ completed(Sub-Trace 完成)
-- `summary` 包含格式化的汇总结果(explore 模式会汇总所有分支)
-
-**Goal 操作**(通过 goal 工具):
-- `add` - 添加顶层目标
-- `under` - 在指定目标下添加子目标
-- `after` - 在指定目标后添加兄弟目标
-- `focus` - 切换焦点到指定目标
-- `done` - 完成当前目标(附带 summary)
-- `abandon` - 放弃当前目标(附带原因)
-
-**实现**:`agent/trace/goal_models.py`, `agent/trace/goal_tool.py`
+# 安装 Gateway 客户端
+cd gateway
+pip install -e .
 
 
-### Message(执行消息)
-
-对应 LLM API 的消息,每条 Message 关联一个 Goal。消息通过 `parent_sequence` 形成树结构。
-
-```python
-@dataclass
-class Message:
-    message_id: str                          # 格式:{trace_id}-{sequence:04d}
-    trace_id: str
-    role: Literal["system", "user", "assistant", "tool"]
-    sequence: int                            # 全局顺序(递增,不复用)
-    parent_sequence: Optional[int] = None    # 父消息的 sequence(构成消息树)
-    goal_id: Optional[str] = None            # 关联的 Goal ID(初始消息为 None,系统会按需自动创建 root goal 兜底)
-    description: str = ""                    # 系统自动生成的摘要
-    tool_call_id: Optional[str] = None
-    content: Any = None
-
-    # 统计
-    prompt_tokens: Optional[int] = None
-    completion_tokens: Optional[int] = None
-    cost: Optional[float] = None
-    duration_ms: Optional[int] = None
-
-    # LLM 响应信息(仅 role="assistant")
-    finish_reason: Optional[str] = None
-
-    created_at: datetime
-
-    # [已弃用] 由 parent_sequence 树结构替代
-    status: Literal["active", "abandoned"] = "active"
-    abandoned_at: Optional[datetime] = None
+# 使用 CLI
+gateway-cli send --from my-agent --to target-agent --message "Hello"
+gateway-cli list
 ```
 ```
 
 
-**消息树(Message Tree)**:
-
-消息通过 `parent_sequence` 形成树。主路径 = 从 `trace.head_sequence` 沿 parent chain 回溯到 root。
-
-```
-正常对话:1 → 2 → 3 → 4 → 5       (每条的 parent 指向前一条)
-Rewind 到 3:3 → 6(parent=3) → 7   (新主路径,4-5 自动脱离)
-压缩 1-3:   8(summary, parent=None) → 6 → 7  (summary 跳过被压缩的消息)
-反思分支:   5 → 9(reflect, parent=5) → 10     (侧枝,不在主路径上)
-```
-
-`build_llm_messages` = 从 head 沿 parent_sequence 链回溯到 root,反转后返回。
-
-Message 提供格式转换方法:
-- `to_llm_dict()` → OpenAI 格式 Dict(用于 LLM 调用)
-- `from_llm_dict(d, trace_id, sequence, goal_id)` → 从 OpenAI 格式创建 Message
-
-**实现**:`agent/trace/models.py`
-
----
-
-## Agent 预设
-
-不同类型 Agent 的配置模板,控制工具权限和参数。
-
-```python
-@dataclass
-class AgentPreset:
-    allowed_tools: Optional[List[str]] = None  # None 表示允许全部
-    denied_tools: Optional[List[str]] = None   # 黑名单
-    max_iterations: int = 30
-    temperature: Optional[float] = None
-    skills: Optional[List[str]] = None         # 注入 system prompt 的 skill 名称列表;None = 加载全部
-    description: Optional[str] = None
-
-
-_DEFAULT_SKILLS = ["planning", "research", "browser"]
-
-AGENT_PRESETS = {
-    "default": AgentPreset(
-        allowed_tools=None,
-        max_iterations=30,
-        skills=_DEFAULT_SKILLS,
-        description="默认 Agent,拥有全部工具权限",
-    ),
-    "explore": AgentPreset(
-        allowed_tools=["read", "glob", "grep", "list_files"],
-        denied_tools=["write", "edit", "bash", "task"],
-        max_iterations=15,
-        skills=["planning"],
-        description="探索型 Agent,只读权限,用于代码分析",
-    ),
-    "analyst": AgentPreset(
-        allowed_tools=["read", "glob", "grep", "web_search", "webfetch"],
-        denied_tools=["write", "edit", "bash", "task"],
-        temperature=0.3,
-        max_iterations=25,
-        skills=["planning", "research"],
-        description="分析型 Agent,用于深度分析和研究",
-    ),
-}
-```
-
-**实现**:`agent/core/presets.py`
-
-**用户自定义**:项目级配置文件(如 `examples/how/presets.json`)可通过 `register_preset()` 注册额外预设。项目专用的 Agent 类型建议放在项目目录下,而非内置预设。
-
----
-
-## 子 Trace 机制
-
-通过 `agent` 工具创建子 Agent 执行任务。`task` 参数为字符串时为单任务(delegate),为列表时并行执行多任务(explore)。支持通过 `messages` 参数预置消息,通过 `continue_from` 参数续跑已有 Sub-Trace。
-
-`agent` 工具负责创建 Sub-Trace 和初始化 GoalTree(因为需要设置自定义 context 元数据和命名规则),创建完成后将 `trace_id` 传给 `RunConfig`,由 Runner 接管后续执行。工具同时维护父 Trace 的 `context["collaborators"]` 列表。
-
-### agent 工具
-
-```python
-@tool(description="创建 Agent 执行任务")
-async def agent(
-    task: Union[str, List[str]],
-    messages: Optional[Union[Messages, List[Messages]]] = None,
-    continue_from: Optional[str] = None,
-    agent_type: Optional[str] = None,
-    skills: Optional[List[str]] = None,
-    context: Optional[dict] = None,
-) -> Dict[str, Any]:
-```
-
-- `agent_type`: 子 Agent 类型,决定工具权限和默认 skills(对应 `AgentPreset` 名称,如 `"deconstruct"`)
-- `skills`: 覆盖 preset 默认值,显式指定注入 system prompt 的 skill 列表
-
-**单任务(delegate)**:`task: str`
-- 创建单个 Sub-Trace
-- 完整工具权限(除 agent/evaluate 外,防止递归)
-- 支持 `continue_from` 续跑已有 Sub-Trace
-- 支持 `messages` 预置上下文消息
-
-**多任务(explore)**:`task: List[str]`
-- 使用 `asyncio.gather()` 并行执行所有任务
-- 每个任务创建独立的 Sub-Trace
-- 只读工具权限(read_file, grep_content, glob_files, goal)
-- `messages` 支持 1D(共享)或 2D(per-agent)
-- 不支持 `continue_from`
-- 汇总所有分支结果返回
-
-### evaluate 工具
-
-```python
-@tool(description="评估目标执行结果是否满足要求")
-async def evaluate(
-    messages: Optional[Messages] = None,
-    target_goal_id: Optional[str] = None,
-    continue_from: Optional[str] = None,
-    context: Optional[dict] = None,
-) -> Dict[str, Any]:
-```
-
-- 代码自动从 GoalTree 注入目标描述(无需 criteria 参数)
-- 模型把执行结果和上下文放在 `messages` 中
-- `target_goal_id` 默认为当前 goal_id
-- 只读工具权限
-- 返回评估结论和改进建议
-
-### 消息类型别名
-
-定义在 `agent/trace/models.py`,用于工具参数和 runner/LLM API 接口:
-
-```python
-ChatMessage = Dict[str, Any]                          # 单条 OpenAI 格式消息
-Messages = List[ChatMessage]                          # 消息列表
-MessageContent = Union[str, List[Dict[str, str]]]     # content 字段(文本或多模态)
-```
-
-**实现位置**:`agent/tools/builtin/subagent.py`
-
-**详细文档**:[工具系统 - Agent/Evaluate 工具](./tools.md#agent-工具)
-
-### ask_human 工具
-
-创建阻塞式 Trace,等待人类通过 IM/邮件等渠道回复。
-
-**注意**:此功能规划中,暂未实现。
-
----
-
-## Active Collaborators(活跃协作者)
-
-任务执行中与模型密切协作的实体(子 Agent 或人类),按 **与当前任务的关系** 分类,而非按 human/agent 分类:
-
-| | 持久存在(外部可查) | 任务内活跃(需要注入) |
-|---|---|---|
-| Agent | 专用 Agent(代码审查等) | 当前任务创建的子 Agent |
-| Human | 飞书通讯录 | 当前任务中正在对接的人 |
-
-### 数据模型
-
-活跃协作者存储在 `trace.context["collaborators"]`:
-
-```python
-{
-    "name": "researcher",            # 名称(模型可见)
-    "type": "agent",                 # agent | human
-    "trace_id": "abc-@delegate-001", # trace_id(agent 场景)
-    "status": "completed",           # running | waiting | completed | failed
-    "summary": "方案A最优",          # 最近状态摘要
-}
-```
-
-### 注入方式
-
-与 GoalTree 一同周期性注入(每 10 轮),渲染为 Markdown:
-
-```markdown
-## Active Collaborators
-- researcher [agent, completed]: 方案A最优
-- 谭景玉 [human, waiting]: 已发送方案确认,等待回复
-- coder [agent, running]: 正在实现特征提取模块
-```
-
-列表为空时不注入。
-
-### 维护
-
-各工具负责更新 collaborators 列表(通过 `context["store"]` 写入 trace.context):
-- `agent` 工具:创建/续跑子 Agent 时更新
-- `feishu` 工具:发送消息/收到回复时更新
-- Runner 只负责读取和注入
-
-**持久联系人/Agent**:通过工具按需查询(如 `feishu_get_contact_list`),不随任务注入。
-
-**实现**:`agent/core/runner.py:AgentRunner._build_context_injection`, `agent/tools/builtin/subagent.py`
-
----
-
-## 工具系统
-
-### 核心概念
-
-```python
-@tool()
-async def my_tool(arg: str, ctx: ToolContext) -> ToolResult:
-    return ToolResult(
-        title="Success",
-        output="Result content",
-        long_term_memory="Short summary"  # 可选:压缩后保留的摘要
-    )
-```
-
-| 类型 | 作用 |
-|------|------|
-| `@tool` | 装饰器,自动注册工具并生成 Schema |
-| `ToolResult` | 工具执行结果,支持双层记忆 |
-| `ToolContext` | 工具执行上下文,依赖注入 |
-
-### 工具分类
-
-| 目录 | 工具 | 说明 |
-|-----|------|------|
-| `trace/` | goal | Agent 内部计划管理 |
-| `builtin/` | agent, evaluate | 子 Agent 创建与评估 |
-| `builtin/file/` | read, write, edit, glob, grep | 文件操作 |
-| `builtin/browser/` | browser actions | 浏览器自动化 |
-| `builtin/` | bash, sandbox, search, webfetch, skill, ask_human | 其他工具 |
-
-### 双层记忆管理
-
-大输出(如网页抓取)只传给 LLM 一次,之后用摘要替代:
-
-```python
-ToolResult(
-    output="<10K tokens 的完整内容>",
-    long_term_memory="Extracted 10000 chars from amazon.com",
-    include_output_only_once=True
-)
-```
-
-**详细文档**:[工具系统](./tools.md)
-
----
-
-## Skills 系统
-
-### 分类
-
-| 类型 | 加载位置 | 加载时机 |
-|------|---------|---------|
-| **内置 Skill** | System Prompt | Agent 启动时自动注入 |
-| **项目 Skill** | System Prompt | Agent 启动时按 preset/call-site 过滤后注入 |
-| **普通 Skill** | 对话消息 | 模型调用 `skill` 工具时 |
-
-### 目录结构
-
-```
-agent/memory/skills/         # 内置 Skills(始终加载)
-├── planning.md              # 计划与 Goal 工具使用
-├── research.md              # 搜索与内容研究
-└── browser.md               # 浏览器自动化
-
-./skills/                    # 项目自定义 Skills
-```
-
-### Skills 过滤(call-site 选择)
-
-不同 Agent 类型所需的 skills 不同。过滤优先级:
-
-1. `agent()` 工具的 `skills` 参数(显式指定,最高优先级)
-2. `AgentPreset.skills`(preset 默认值)
-3. `None`(加载全部,向后兼容)
-
-示例:调用子 Agent 时只注入解构相关 skill:
-```python
-agent(task="...", agent_type="deconstruct", skills=["planning", "deconstruct"])
-```
-
-**实现**:`agent/memory/skill_loader.py`
-
-**详细文档**:[Skills 使用指南](./skills.md)
-
----
-
-## Experiences 系统
-
-从执行历史中提取的经验规则,用于指导未来任务。
-
-### 存储规范
-
-经验以 Markdown 文件存储(默认 `./.cache/experiences.md`),人类可读、可编辑、可版本控制。
-
-文件格式:
-
-```markdown
----
-id: ex_001
-trace_id: trace-xxx
-category: tool_usage
-tags: {state: ["large_file", "dirty_repo"], intent: ["batch_edit", "safe_modify"]}
-metrics: {helpful: 12, harmful: 0}
-created_at: 2026-02-12 15:30
----
-
----
-id: ex_002
-...
-```
----
-
-
-### 反思机制(Reflect)
-
-通过 POST /api/traces/{id}/reflect 触发,旨在将原始执行历史提炼为可复用的知识。
-    1. 分叉反思:在 trace 末尾追加 user message(含反思与打标 Prompt),作为侧枝执行。
-    2. 结构化生成:
-        ·归类:将经验分配至 tool_usage(工具)、logic_flow(逻辑)、environment(环境)等。
-        ·打标:提取 state(环境状态)与 intent(用户意图)语义标签。
-        ·量化:初始 helpful 设为 1。
-    3. 持久化:将带有元数据的 Markdown 块追加至 experiences.md。
-
-实现:agent/trace/run_api.py:reflect_trace
-
-### 语义注入与匹配流程
-新建 Trace 时,Runner 采用“分析-检索-注入”三阶段策略,实现精准经验推荐。
-    1. 意图预分析
-    Runner 调用 utility_llm 对初始任务进行语义提取:
-        -输入:"优化这个项目的 Docker 构建速度"
-        -输出:{state: ["docker", "ci"], intent: ["optimization"]}
-    2. 语义检索
-        在 _load_experiences 中根据标签进行语义匹配(优先匹配 intent,其次是 state),筛选出相关度最高的 Top-K 条经验。
-    3. 精准注入
-        将匹配到的经验注入第一条 user message 末尾:
-```python
-# _build_history 中(仅新建模式):
-if not config.trace_id:
-    relevant_ex = self.experience_retriever.search(task_tags)
-    if relevant_ex:
-        formatted_ex = "\n".join([f"- [{e.id}] {e.content} (Helpful: {e.helpful})" for e in relevant_ex])
-        first_user_msg["content"] += f"\n\n## 参考经验\n\n{formatted_ex}"
-```
-实现:agent/core/runner.py:AgentRunner._build_history
-
-### 经验获取工具
-不再仅限于启动时自动注入,而是通过内置工具供 Agent 在需要时主动调用。当执行结果不符合预期或进入未知领域时,Agent 应优先使用此工具。
-工具定义:
-
-```python
-@tool(description="根据当前任务状态和意图,从经验库中检索相关的历史经验")
-async def get_experience(
-    intent: Optional[str] = None, 
-    state: Optional[str] = None
-) -> Dict[str, Any]:
-    """
-    参数:
-        intent: 想要达成的目标意图 (如 "optimization", "debug")
-        state: 当前环境或遇到的问题状态 (如 "docker_build_fail", "permission_denied")
-    """
-```
-实现: agent/tools/builtin/experience.py
-
-- 语义匹配与应用流程
-    当 Agent 调用 get_experience 时,系统执行以下逻辑:
-    1. 语义检索:根据传入的 intent 或 state 标签,在 experiences.md 中进行匹配。匹配权重:intent > state > helpful 评分。
-    2. 动态注入:工具返回匹配到的 Top-K 条经验(含 ID 和内容)。
-    3. 策略应用:Agent 接收到工具返回的经验后,需在后续 thought 中声明所选用的策略 ID(如 [ex_001]),并据此调整 goal_tree 或工具调用序列。
-
-## Context 压缩
-
-### 两级压缩策略
-
-#### Level 1:GoalTree 过滤(确定性,零成本)
-
-每轮 agent loop 构建 `llm_messages` 时自动执行:
-- 始终保留:system prompt、第一条 user message(含 GoalTree 精简视图)、当前 focus goal 的消息
-- 跳过 completed/abandoned goals 的消息(信息已在 GoalTree summary 中)
-- 通过 Message Tree 的 parent_sequence 实现跳过
-
-大多数情况下 Level 1 足够。
-
-#### Level 2:LLM 总结(仅在 Level 1 后仍超限时触发)
-
-触发条件:Level 1 之后 token 数仍超过阈值(默认 `max_tokens × 0.8`)。
-
-流程:
-1. **经验提取**:先在消息列表末尾追加反思 prompt → 主模型回复 → 追加到 `./.cache/experiences.md`。反思消息为侧枝(parent_sequence 分叉,不在主路径上)
-2. **压缩**:在消息列表末尾追加压缩 prompt(含 GoalTree 完整视图) → 主模型回复 → summary 存为新消息,其 `parent_sequence` 跳过被压缩的范围
-
-### GoalTree 双视图
-
-`to_prompt()` 支持两种模式:
-- `include_summary=False`(默认):精简视图,用于日常周期性注入
-- `include_summary=True`:含所有 completed goals 的 summary,用于 Level 2 压缩时提供上下文
-
-### 压缩存储
-
-- 原始消息永远保留在 `messages/`
-- 压缩 summary 作为普通 Message 存储
-- 通过 `parent_sequence` 树结构实现跳过,无需 compression events 或 skip list
-- Rewind 到压缩区域内时,summary 脱离主路径,原始消息自动恢复
-
-**实现**:`agent/trace/compaction.py`, `agent/trace/goal_models.py`
-
-**详细文档**:[Context 管理](./context-management.md)
-
----
-
-## 存储接口
-
-```python
-class TraceStore(Protocol):
-    async def create_trace(self, trace: Trace) -> None: ...
-    async def get_trace(self, trace_id: str) -> Trace: ...
-    async def update_trace(self, trace_id: str, **updates) -> None: ...
-    async def add_message(self, message: Message) -> None: ...
-    async def get_trace_messages(self, trace_id: str) -> List[Message]: ...
-    async def get_main_path_messages(self, trace_id: str, head_sequence: int) -> List[Message]: ...
-    async def get_messages_by_goal(self, trace_id: str, goal_id: str) -> List[Message]: ...
-    async def append_event(self, trace_id: str, event_type: str, payload: Dict) -> int: ...
-```
-
-`get_main_path_messages` 从 `head_sequence` 沿 `parent_sequence` 链回溯,返回主路径上的有序消息列表。
-
-**实现**:
-- 协议定义:`agent/trace/protocols.py`
-- 文件存储:`agent/trace/store.py:FileSystemTraceStore`
-
-### 存储结构
-
-```
-.trace/
-├── {trace_id}/
-│   ├── meta.json        # Trace 元数据(含 tools 定义)
-│   ├── goal.json        # GoalTree(mission + goals 列表)
-│   ├── events.jsonl     # 事件流(goal 变更、sub_trace 生命周期等)
-│   └── messages/        # Messages
-│       ├── {trace_id}-0001.json
-│       └── ...
-│
-└── {trace_id}@explore-{序号}-{timestamp}-001/  # 子 Trace
-    └── ...
-```
-
-**events.jsonl 说明**:
-- 记录 Trace 执行过程中的关键事件
-- 每行一个 JSON 对象,包含 event_id、event 类型、时间戳等
-- 主要事件类型:goal_added, goal_updated, sub_trace_started, sub_trace_completed, rewind
-- 用于实时监控和历史回放
-
-**Sub-Trace 目录命名**:
-- Explore: `{parent}@explore-{序号:03d}-{timestamp}-001`
-- Delegate: `{parent}@delegate-{timestamp}-001`
-- Evaluate: `{parent}@evaluate-{timestamp}-001`
-
-**meta.json 示例**:
-```json
-{
-  "trace_id": "0415dc38-...",
-  "mode": "agent",
-  "task": "分析代码结构",
-  "agent_type": "default",
-  "status": "running",
-  "model": "google/gemini-2.5-flash",
-  "tools": [...],
-  "llm_params": {"temperature": 0.3},
-  "context": {
-    "collaborators": [
-      {"name": "researcher", "type": "agent", "trace_id": "...", "status": "completed", "summary": "方案A最优"}
-    ]
-  },
-  "current_goal_id": "3"
-}
-```
-
----
-
-## 设计决策
-
-详见 [设计决策文档](./decisions.md)
-
-**核心决策**:
-
-1. **所有 Agent 都是 Trace** - 主 Agent、子 Agent、人类协助统一为 Trace,通过 `parent_trace_id` 和 `spawn_tool` 区分
-
-2. **trace/ 模块统一管理执行状态** - 合并原 execution/ 和 goal/,包含计划管理和 Agent 内部控制工具
-
-3. **tools/ 专注外部交互** - 文件、命令、网络、浏览器等与外部世界的交互
-
-4. **Agent 预设替代 Sub-Agent 配置** - 通过 `core/presets.py` 定义不同类型 Agent 的工具权限和参数
+详见:[Gateway README](../gateway/README.md) 和 [A2A IM 文档](../gateway/client/a2a_im.md)
 
 
 ---
 ---
 
 
 ## 相关文档
 ## 相关文档
 
 
-| 文档 | 内容 |
-|-----|------|
-| [Context 管理](./context-management.md) | Goals、压缩、Plan 注入策略 |
-| [工具系统](./tools.md) | 工具定义、注册、双层记忆 |
-| [Skills 指南](./skills.md) | Skill 分类、编写、加载 |
-| [多模态支持](./multimodal.md) | 图片、PDF 处理 |
-| [设计决策](./decisions.md) | 架构决策记录 |
-| [测试指南](./testing.md) | 测试策略和命令 |
+完整的文档列表见各模块的 README:
+- [Agent Core 文档](../agent/README.md#文档)
+- [Gateway 文档](../gateway/README.md#文档)

+ 651 - 0
docs/a2a-im.md

@@ -0,0 +1,651 @@
+# A2A IM:Agent 即时通讯系统
+
+**更新日期:** 2026-03-04
+
+## 文档维护规范
+
+0. **先改文档,再动代码** - 新功能或重大修改需先完成文档更新、并完成审阅后,再进行代码实现;除非改动较小、不被文档涵盖
+1. **文档分层,链接代码** - 重要或复杂设计可以另有详细文档;关键实现需标注代码文件路径;格式:`module/file.py:function_name`
+2. **简洁快照,日志分离** - 只记录最重要的、与代码准确对应的或者明确的已完成的设计的信息,避免推测、建议,或大量代码;决策依据或修改日志若有必要,可在`docs/decisions.md`另行记录
+
+---
+
+## 文档说明
+
+本文档描述 Agent 间即时通讯(A2A IM)系统的架构和实现。
+
+**相关文档**:
+- [MAMP 协议](./research/a2a-mamp-protocol.md):消息格式和传输协议
+- [A2A 跨设备通信](./research/a2a-cross-device.md):内部 Agent 通信方案
+- [Agent 框架](./README.md):核心 Agent 能力
+- [Enterprise 层](../gateway/docs/enterprise/overview.md):组织级功能
+
+---
+
+## 系统概述
+
+A2A IM 是一个**任务导向的 Agent 即时通讯系统**,支持:
+- Agent 间消息传递(点对点、通过 Gateway)
+- 活跃协作者管理(当前任务)
+- 全局联系人管理(历史记录)
+- 在线状态查询
+- 对话历史追溯
+
+**与传统 IM 的区别**:
+- 任务导向(非纯聊天)
+- 长时间处理(分钟到小时)
+- 工具调用和执行记录
+- 完整的 Trace 追溯
+
+---
+
+## 架构层次关系
+
+A2A IM 在整体架构中的定位:
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ Enterprise Layer(组织级)- 可选                              │
+│ - 认证和授权(飞书 OAuth、API Key、JWT)                      │
+│ - 审计和监控(操作日志、成本记录、安全事件)                    │
+│ - 多租户和权限控制(角色验证、资源访问控制)                    │
+│ - 成本管理和限额(用户级/组织级限额、超限告警)                 │
+│                                                              │
+│ 实现位置: gateway/enterprise/                                │
+│ 文档: gateway/docs/enterprise/overview.md                   │
+└─────────────────────────────────────────────────────────────┘
+
+┌─────────────────────────────────────────────────────────────┐
+│ A2A IM Gateway(通讯层)★ 本文档                             │
+│ - Agent 注册和发现(Registry)                                │
+│ - 消息路由(Gateway Router)                                  │
+│ - 活跃协作者管理(Collaborators)                             │
+│ - 在线状态管理(Heartbeat)                                   │
+│ - 联系人管理(ContactStore)                                  │
+│                                                              │
+│ 实现位置: gateway/core/                                       │
+│ 文档: docs/a2a-im.md(本文档)                                │
+└─────────────────────────────────────────────────────────────┘
+         ↕ 使用(单向依赖)
+┌─────────────────────────────────────────────────────────────┐
+│ Agent Core(核心层)                                          │
+│ - Trace、Message、Goal 管理                                  │
+│ - 工具系统(文件、命令、网络、浏览器)                          │
+│ - LLM 集成(Gemini、OpenRouter、Yescode)                    │
+│ - Skills(领域知识注入)                                        │
+│ - 子 Agent 机制(agent 工具)                                 │
+│                                                              │
+│ 实现位置: agent/                                              │
+│ 文档: docs/README.md                                         │
+└─────────────────────────────────────────────────────────────┘
+```
+
+### 层次说明
+
+**Agent Core(核心层)**:
+- 提供单个 Agent 的执行能力
+- 管理 Trace、Message、Goal
+- 提供工具系统和 LLM 集成
+- 支持子 Agent 创建(通过 `agent` 工具)
+- **独立部署**:可以不依赖 Gateway 运行
+
+**A2A IM Gateway(通讯层)**:
+- 与 Agent Core 并列,独立的系统
+- 提供 Agent 间通讯能力
+- 管理 Agent 注册和在线状态
+- 路由消息到目标 Agent
+- 维护活跃协作者和联系人
+- **依赖 Agent Core**:使用 ToolContext、TraceStore 等组件
+- **独立部署**:可以作为独立服务部署
+
+**Enterprise(组织层)**:
+- 可选的企业功能扩展
+- 提供企业级管理和控制
+- 认证、授权、审计
+- 多租户和成本管理
+- **可以集成到 Gateway**:作为 Gateway 的扩展模块
+- **也可以独立部署**:作为独立的 Enterprise Gateway 服务
+
+### 依赖关系
+
+```
+Enterprise → Gateway → Agent Core
+(可选)    (通讯)    (核心)
+
+- Agent Core 不依赖任何其他层(独立)
+- Gateway 依赖 Agent Core(单向依赖)
+- Enterprise 依赖 Gateway(可选扩展)
+```
+
+### 部署方式
+
+**方式 1:单体部署(个人/小团队)**
+```
+一个进程:
+├─ Agent Core
+└─ Gateway(包含 Enterprise 模块)
+```
+
+**方式 2:分离部署(中等规模)**
+```
+进程 1:Agent Core
+进程 2:Gateway(包含 Enterprise 模块)
+```
+
+**方式 3:分层部署(大规模/企业)**
+```
+进程 1:Agent Core
+进程 2:Gateway Core
+进程 3:Enterprise Gateway
+```
+
+---
+
+## 架构设计
+
+### 三层架构
+
+```
+┌─────────────────────────────────────────────────┐
+│ Layer 3: Agent 逻辑层                            │
+│ - Trace, Goal, Messages                         │
+│ - 工具调用和执行                                  │
+└─────────────────────────────────────────────────┘
+                    ↕
+┌─────────────────────────────────────────────────┐
+│ Layer 2: A2A IM 层                               │
+│ - 活跃协作者管理                                  │
+│ - 全局联系人管理                                  │
+│ - conversation_id ↔ trace_id 映射                │
+└─────────────────────────────────────────────────┘
+                    ↕
+┌─────────────────────────────────────────────────┐
+│ Layer 1: Gateway 层                              │
+│ - Agent 注册和发现                                │
+│ - 消息路由                                        │
+│ - 在线状态管理                                    │
+│ - WebSocket 长连接                                │
+└─────────────────────────────────────────────────┘
+```
+
+### 通信模式
+
+**模式 1:内部 Agent(同进程)**
+```
+Agent A → 直接调用 → Agent B
+(复用现有 agent 工具)
+```
+
+**模式 2:跨设备 Agent(组织内)**
+```
+PC Agent → WebSocket → Gateway → 云端 Agent
+(反向连接,无需公网 IP)
+```
+
+**模式 3:外部 Agent(跨组织)**
+```
+Agent A → MAMP 协议 → Agent B
+(点对点 HTTP)
+```
+
+---
+
+## 数据模型
+
+### 活跃协作者(Layer 2)
+
+存储在 `trace.context["collaborators"]`,记录当前任务的协作者。
+
+```python
+{
+    "name": "code-analyst",
+    "type": "agent",  # agent | human
+    "agent_uri": "agent://other.com/code-analyst",
+    "trace_id": "abc-123",
+    "conversation_id": "conv-456",
+    "status": "running",  # running | waiting | completed | failed
+    "summary": "正在分析代码架构",
+    "last_message_at": "2026-03-04T10:30:00Z"
+}
+```
+
+**实现位置**:`agent/core/runner.py:AgentRunner._build_context_injection`
+
+### 全局联系人(Layer 2)
+
+存储在 `.trace/contacts.json`,记录所有历史联系过的 Agent。
+
+```python
+{
+    "agent_uri": "agent://other.com/code-analyst",
+    "name": "Code Analyst",
+    "type": "agent",
+
+    # 身份信息(从 Agent Card 获取)
+    "card": {
+        "description": "专注于代码分析",
+        "capabilities": ["code_analysis", "file_read"],
+        "owner": {"user_name": "张三"}
+    },
+
+    # 交互统计
+    "stats": {
+        "first_contact": "2026-02-01T10:00:00Z",
+        "last_contact": "2026-03-04T10:30:00Z",
+        "total_conversations": 15,
+        "total_messages": 127
+    },
+
+    # 最近对话
+    "recent_conversations": [
+        {
+            "conversation_id": "conv-456",
+            "trace_id": "abc-123",
+            "started_at": "2026-03-04T10:00:00Z",
+            "last_message": "分析完成",
+            "status": "active"
+        }
+    ],
+
+    # 关系标签
+    "tags": ["code", "architecture"],
+    "pinned": false
+}
+```
+
+**实现位置**:`agent/trace/contact_store.py`
+
+### Agent 注册信息(Layer 1)
+
+存储在 Gateway,记录在线 Agent 的连接信息。
+
+```python
+{
+    "agent_uri": "agent://internal/code-analyst",
+    "connection_type": "websocket",  # websocket | http
+    "websocket": <WebSocket>,  # WebSocket 连接对象
+    "http_endpoint": "http://localhost:8001",  # HTTP 端点
+    "last_heartbeat": "2026-03-04T10:30:00Z",
+    "capabilities": ["code_analysis", "file_read"]
+}
+```
+
+**实现位置**:`gateway/core/registry.py`
+
+---
+
+## 核心功能
+
+### 1. Agent 注册和发现
+
+**PC Agent 启动时注册**:
+
+```python
+# 建立 WebSocket 长连接
+ws = await websockets.connect("wss://gateway.com/gateway/connect")
+
+# 注册
+await ws.send(json.dumps({
+    "type": "register",
+    "agent_uri": "agent://internal/my-agent",
+    "capabilities": ["file_read", "bash"]
+}))
+
+# 保持心跳
+while True:
+    await ws.send(json.dumps({"type": "heartbeat"}))
+    await asyncio.sleep(30)
+```
+
+**实现位置**:`gateway/core/client.py`
+
+### 2. 消息路由
+
+**通过 Gateway 发送消息**:
+
+```python
+# 发送方
+POST /gateway/send
+{
+    "to": "agent://internal/code-analyst",
+    "content": "帮我分析代码"
+}
+
+# Gateway 查找目标 Agent
+agent_info = registry.lookup("agent://internal/code-analyst")
+
+# 通过 WebSocket 推送
+await agent_info["websocket"].send(json.dumps({
+    "type": "message",
+    "from": "agent://internal/caller",
+    "content": "帮我分析代码"
+}))
+```
+
+**实现位置**:`gateway/core/router.py`
+
+### 3. 活跃协作者管理
+
+**发送消息时自动更新**:
+
+```python
+# agent/tools/builtin/a2a_im.py
+
+async def send_to_agent(...):
+    # 发送消息
+    response = await gateway_client.send(...)
+
+    # 更新活跃协作者
+    await update_active_collaborator(
+        trace_id=ctx.trace_id,
+        agent_uri=target_agent,
+        conversation_id=response["conversation_id"],
+        status="waiting"
+    )
+```
+
+**周期性注入到 Agent 上下文**:
+
+```python
+# agent/core/runner.py
+
+if iteration % 10 == 0:
+    collaborators = trace.context.get("collaborators", [])
+    inject_collaborators_markdown(collaborators)
+```
+
+### 4. 全局联系人管理
+
+**查询联系人**:
+
+```python
+# 通过工具查询
+contacts = await get_contacts(
+    type="agent",
+    status="online",
+    tags=["code"]
+)
+```
+
+**自动维护**:
+
+```python
+# 发送/接收消息时自动更新
+await contact_store.update(
+    agent_uri=target_agent,
+    last_contact=datetime.now(),
+    increment_message_count=True
+)
+```
+
+**实现位置**:`agent/trace/contact_store.py`
+
+### 5. 在线状态查询
+
+**查询 Agent 在线状态**:
+
+```python
+GET /gateway/status/{agent_uri}
+
+返回:
+{
+    "agent_uri": "agent://internal/code-analyst",
+    "status": "online",  # online | offline
+    "last_seen": "2026-03-04T10:30:00Z"
+}
+```
+
+**实现位置**:`gateway/core/router.py:get_agent_status`
+
+---
+
+## API 端点
+
+### Gateway API
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| WS | `/gateway/connect` | Agent 注册和保持连接 |
+| POST | `/gateway/send` | 发送消息到其他 Agent |
+| GET | `/gateway/status/{agent_uri}` | 查询 Agent 在线状态 |
+| GET | `/gateway/agents` | 列出所有在线 Agent |
+
+### A2A IM API
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| GET | `/api/traces/{id}/collaborators` | 查询活跃协作者 |
+| GET | `/api/contacts` | 查询全局联系人 |
+| GET | `/api/contacts/{agent_uri}` | 查询特定联系人详情 |
+| GET | `/api/contacts/{agent_uri}/conversations` | 查询对话历史 |
+
+---
+
+## 工具系统
+
+### send_to_agent 工具
+
+发送消息到其他 Agent(内部或外部)。
+
+```python
+@tool(description="发送消息到其他 Agent")
+async def send_to_agent(
+    target_agent: str,  # agent://domain/id
+    message: str,
+    conversation_id: Optional[str] = None,
+    ctx: ToolContext = None
+) -> ToolResult
+```
+
+**实现位置**:`agent/tools/builtin/a2a_im.py:send_to_agent`
+
+### get_active_collaborators 工具
+
+查询当前任务的活跃协作者。
+
+```python
+@tool(description="查询当前任务的活跃协作者")
+async def get_active_collaborators(
+    ctx: ToolContext
+) -> ToolResult
+```
+
+**实现位置**:`agent/tools/builtin/a2a_im.py:get_active_collaborators`
+
+### get_contacts 工具
+
+查询全局联系人列表。
+
+```python
+@tool(description="查询所有联系过的 Agent")
+async def get_contacts(
+    type: Optional[str] = None,  # agent | human
+    status: Optional[str] = None,  # online | offline
+    tags: Optional[List[str]] = None,
+    ctx: ToolContext = None
+) -> ToolResult
+```
+
+**实现位置**:`agent/tools/builtin/a2a_im.py:get_contacts`
+
+---
+
+## Skill 系统
+
+### a2a_im.md Skill
+
+提供 A2A IM 使用指南,注入到 Agent 的 system prompt。
+
+**内容**:
+- 如何发送消息到其他 Agent
+- 如何查询活跃协作者
+- 如何查询联系人
+- 最佳实践
+
+**实现位置**:`agent/skill/skills/a2a_im.md`
+
+---
+
+## 使用示例
+
+### 场景 1:调用其他 Agent 协作
+
+```python
+# Agent A 需要代码分析帮助
+result = await send_to_agent(
+    target_agent="agent://internal/code-analyst",
+    message="帮我分析 /path/to/project 的架构"
+)
+
+# 继续对话
+result2 = await send_to_agent(
+    target_agent="agent://internal/code-analyst",
+    message="重点分析 core 模块",
+    conversation_id=result["conversation_id"]
+)
+```
+
+### 场景 2:查询活跃协作者
+
+```python
+# 查看当前任务中有哪些 Agent 在协作
+collaborators = await get_active_collaborators()
+
+# 输出:
+# ## 活跃协作者
+# - code-analyst [agent, completed]: 分析完成,发现3个问题
+# - test-runner [agent, running]: 正在运行测试
+```
+
+### 场景 3:查询联系人
+
+```python
+# 查找擅长代码分析的 Agent
+contacts = await get_contacts(
+    type="agent",
+    tags=["code", "architecture"]
+)
+
+# 输出:
+# ## 联系人列表
+# 🟢 code-analyst - agent://internal/code-analyst
+#    最后联系: 2026-03-04 10:30
+#    对话次数: 15
+```
+
+---
+
+## 架构决策
+
+### 决策 1:Gateway 与 Agent 并列而非包含
+
+**问题**:Gateway 应该放在 agent/ 内部还是与 agent/ 并列?
+
+**决策**:与 agent/ 并列
+
+**理由**:
+1. **解耦**:Gateway 和 Agent Core 是两个独立的系统
+2. **独立部署**:Gateway 可以独立部署和扩展
+3. **职责清晰**:Agent Core 负责单 Agent 执行,Gateway 负责 Agent 间通讯
+4. **依赖关系**:Gateway 依赖 Agent Core(单向),但 Agent Core 不依赖 Gateway
+
+**实现**:
+- 目录结构:`gateway/` 与 `agent/` 并列
+- Import 路径:`from gateway.core import ...`
+
+### 决策 2:Enterprise 与 Gateway 的关系
+
+**问题**:Enterprise 应该是 Gateway 的上层(分层架构)还是 Gateway 的模块(模块化架构)?
+
+**决策**:根据阶段选择
+
+**MVP 阶段(当前)**:模块化架构
+- Enterprise 作为 Gateway 的可选模块
+- 部署简单,快速迭代
+- 适合中小规模
+
+**大规模阶段(未来)**:可选分层架构
+- Enterprise 作为独立的 Gateway 层
+- 可独立扩容,团队协作
+- 适合大规模部署
+
+**理由**:
+1. **灵活性**:两种架构都可以实现可选部署
+2. **演进路径**:从模块化开始,需要时重构为分层
+3. **规模决定**:小规模用模块化,大规模用分层
+
+**实现**:
+- 当前:`gateway/enterprise/` 作为可选模块
+- 未来:可重构为独立的 `enterprise_gateway/` 服务
+
+### 决策 3:活跃协作者的管理方式
+
+**问题**:活跃协作者信息应该如何存储和管理?
+
+**决策**:存储在 `trace.context["collaborators"]`,由工具自动维护
+
+**理由**:
+1. **复用现有机制**:Agent Core 已有 context 机制
+2. **自动注入**:Runner 周期性注入到 Agent 上下文(每 10 轮)
+3. **工具维护**:send_to_agent 等工具自动更新
+4. **与 Goal 一致**:与 GoalTree 一同注入,保持一致性
+
+**实现位置**:
+- 存储:`trace.context["collaborators"]`
+- 注入:`agent/core/runner.py:AgentRunner._build_context_injection`
+- 更新:`agent/tools/builtin/a2a_im.py:_update_active_collaborator`
+
+---
+
+## 实现路线图
+
+### Phase 1:基础功能(1-2 周)
+
+**目标**:实现核心通信能力
+
+**任务**:
+1. 实现 Gateway(注册、路由、WebSocket)
+2. 实现 send_to_agent 工具
+3. 实现活跃协作者自动更新
+4. 实现 a2a_im.md Skill
+
+**实现位置**:
+- `gateway/core/`
+- `agent/tools/builtin/a2a_im.py`
+- `agent/skill/skills/a2a_im.md`
+
+### Phase 2:联系人管理(1 周)
+
+**目标**:完善联系人和历史记录
+
+**任务**:
+1. 实现 ContactStore
+2. 实现 get_contacts 工具
+3. 实现对话历史查询
+4. 实现在线状态查询
+
+**实现位置**:
+- `agent/trace/contact_store.py`
+- `agent/tools/builtin/a2a_im.py`
+
+### Phase 3:增强功能(可选)
+
+**目标**:提升用户体验
+
+**任务**:
+1. 实现消息队列(异步处理)
+2. 实现 Agent 发现和推荐
+3. 实现关系标签和分组
+4. 实现 UI 界面
+
+---
+
+## 相关文档
+
+- [MAMP 协议](./research/a2a-mamp-protocol.md):消息格式和传输协议
+- [A2A 跨设备通信](./research/a2a-cross-device.md):内部 Agent 通信方案
+- [工具系统](../agent/docs/tools.md):工具定义、注册
+- [Skills 指南](../agent/docs/skills.md):Skill 分类、编写、加载
+- [Agent 框架](./README.md):核心 Agent 能力
+- [Gateway 架构](../gateway/docs/architecture.md):Gateway 三层架构
+- [Gateway API](../gateway/docs/api.md):Gateway API 参考

+ 0 - 78
docs/ref/Claude Code/agent-prompt-agent-creation-architect.md

@@ -1,78 +0,0 @@
-<!--
-name: 'Agent Prompt: Agent creation architect'
-description: System prompt for creating custom AI agents with detailed specifications
-ccVersion: 2.0.77
-variables:
-  - TASK_TOOL_NAME
--->
-You are an elite AI agent architect specializing in crafting high-performance agent configurations. Your expertise lies in translating user requirements into precisely-tuned agent specifications that maximize effectiveness and reliability.
-
-**Important Context**: You may have access to project-specific instructions from CLAUDE.md files and other context that may include coding standards, project structure, and custom requirements. Consider this context when creating agents to ensure they align with the project's established patterns and practices.
-
-When a user describes what they want an agent to do, you will:
-
-1. **Extract Core Intent**: Identify the fundamental purpose, key responsibilities, and success criteria for the agent. Look for both explicit requirements and implicit needs. Consider any project-specific context from CLAUDE.md files. For agents that are meant to review code, you should assume that the user is asking to review recently written code and not the whole codebase, unless the user has explicitly instructed you otherwise.
-
-2. **Design Expert Persona**: Create a compelling expert identity that embodies deep domain knowledge relevant to the task. The persona should inspire confidence and guide the agent's decision-making approach.
-
-3. **Architect Comprehensive Instructions**: Develop a system prompt that:
-   - Establishes clear behavioral boundaries and operational parameters
-   - Provides specific methodologies and best practices for task execution
-   - Anticipates edge cases and provides guidance for handling them
-   - Incorporates any specific requirements or preferences mentioned by the user
-   - Defines output format expectations when relevant
-   - Aligns with project-specific coding standards and patterns from CLAUDE.md
-
-4. **Optimize for Performance**: Include:
-   - Decision-making frameworks appropriate to the domain
-   - Quality control mechanisms and self-verification steps
-   - Efficient workflow patterns
-   - Clear escalation or fallback strategies
-
-5. **Create Identifier**: Design a concise, descriptive identifier that:
-   - Uses lowercase letters, numbers, and hyphens only
-   - Is typically 2-4 words joined by hyphens
-   - Clearly indicates the agent's primary function
-   - Is memorable and easy to type
-   - Avoids generic terms like "helper" or "assistant"
-
-6 **Example agent descriptions**:
-  - in the 'whenToUse' field of the JSON object, you should include examples of when this agent should be used.
-  - examples should be of the form:
-    - <example>
-      Context: The user is creating a test-runner agent that should be called after a logical chunk of code is written.
-      user: "Please write a function that checks if a number is prime"
-      assistant: "Here is the relevant function: "
-      <function call omitted for brevity only for this example>
-      <commentary>
-      Since a significant piece of code was written, use the ${TASK_TOOL_NAME} tool to launch the test-runner agent to run the tests.
-      </commentary>
-      assistant: "Now let me use the test-runner agent to run the tests"
-    </example>
-    - <example>
-      Context: User is creating an agent to respond to the word "hello" with a friendly jok.
-      user: "Hello"
-      assistant: "I'm going to use the ${TASK_TOOL_NAME} tool to launch the greeting-responder agent to respond with a friendly joke"
-      <commentary>
-      Since the user is greeting, use the greeting-responder agent to respond with a friendly joke. 
-      </commentary>
-    </example>
-  - If the user mentioned or implied that the agent should be used proactively, you should include examples of this.
-- NOTE: Ensure that in the examples, you are making the assistant use the Agent tool and not simply respond directly to the task.
-
-Your output must be a valid JSON object with exactly these fields:
-{
-  "identifier": "A unique, descriptive identifier using lowercase letters, numbers, and hyphens (e.g., 'test-runner', 'api-docs-writer', 'code-formatter')",
-  "whenToUse": "A precise, actionable description starting with 'Use this agent when...' that clearly defines the triggering conditions and use cases. Ensure you include examples as described above.",
-  "systemPrompt": "The complete system prompt that will govern the agent's behavior, written in second person ('You are...', 'You will...') and structured for maximum clarity and effectiveness"
-}
-
-Key principles for your system prompts:
-- Be specific rather than generic - avoid vague instructions
-- Include concrete examples when they would clarify behavior
-- Balance comprehensiveness with clarity - every instruction should add value
-- Ensure the agent has enough context to handle variations of the core task
-- Make the agent proactive in seeking clarification when needed
-- Build in quality assurance and self-correction mechanisms
-
-Remember: The agents you create should be autonomous experts capable of handling their designated tasks with minimal additional guidance. Your system prompts are their complete operational manual.

+ 0 - 12
docs/ref/Claude Code/agent-prompt-bash-command-description-writer.md

@@ -1,12 +0,0 @@
-Clear, concise description of what this command does in active voice. Never use words like "complex" or "risk" in the description - just describe what it does.
-
-For simple commands (git, npm, standard CLI tools), keep it brief (5-10 words):
-
-ls → "List files in current directory"
-git status → "Show working tree status"
-npm install → "Install package dependencies"
-For commands that are harder to parse at a glance (piped commands, obscure flags, etc.), add enough context to clarify what it does:
-
-find . -name "*.tmp" -exec rm {} \; → "Find and delete all .tmp files recursively"
-git reset --hard origin/main → "Discard all local changes and match remote main"
-curl -s url | jq '.data[]' → "Fetch JSON from URL and extract data array elements"

+ 0 - 9
docs/ref/Claude Code/system-prompt-doing-tasks.md

@@ -1,9 +0,0 @@
-Doing tasks
-The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended: ${"- NEVER propose changes to code you haven't read. If a user asks about or wants you to modify a file, read it first. Understand existing code before suggesting modifications."}${TOOL_USAGE_HINTS_ARRAY.length>0? ${TOOL_USAGE_HINTS_ARRAY.join( )}:""}
-
-Be careful not to introduce security vulnerabilities such as command injection, XSS, SQL injection, and other OWASP top 10 vulnerabilities. If you notice that you wrote insecure code, immediately fix it.
-Avoid over-engineering. Only make changes that are directly requested or clearly necessary. Keep solutions simple and focused.
-Don't add features, refactor code, or make "improvements" beyond what was asked. A bug fix doesn't need surrounding code cleaned up. A simple feature doesn't need extra configurability. Don't add docstrings, comments, or type annotations to code you didn't change. Only add comments where the logic isn't self-evident.
-Don't add error handling, fallbacks, or validation for scenarios that can't happen. Trust internal code and framework guarantees. Only validate at system boundaries (user input, external APIs). Don't use feature flags or backwards-compatibility shims when you can just change the code.
-Don't create helpers, utilities, or abstractions for one-time operations. Don't design for hypothetical future requirements. The right amount of complexity is the minimum needed for the current task—three similar lines of code is better than a premature abstraction.
-Avoid backwards-compatibility hacks like renaming unused `_vars`, re-exporting types, adding `// removed` comments for removed code, etc. If something is unused, delete it completely.

+ 0 - 6
docs/ref/Claude Code/system-prompt-tool-usage-policy.md

@@ -1,6 +0,0 @@
-Tool usage policy${WEBFETCH_ENABLED_SECTION}${MCP_TOOLS_SECTION}
-You can call multiple tools in a single response. If you intend to call multiple tools and there are no dependencies between them, make all independent tool calls in parallel. Maximize use of parallel tool calls where possible to increase efficiency. However, if some tool calls depend on previous calls to inform dependent values, do NOT call these tools in parallel and instead call them sequentially. For instance, if one operation must complete before another starts, run these operations sequentially instead. Never use placeholders or guess missing parameters in tool calls.
-If the user specifies that they want you to run tools "in parallel", you MUST send a single message with multiple tool use content blocks. For example, if you need to launch multiple agents in parallel, send a single message with multiple ${TASK_TOOL_NAME} tool calls.
-Use specialized tools instead of bash commands when possible, as this provides a better user experience. For file operations, use dedicated tools: ${READ_TOOL_NAME} for reading files instead of cat/head/tail, ${EDIT_TOOL_NAME} for editing instead of sed/awk, and ${WRITE_TOOL_NAME} for creating files instead of cat with heredoc or echo redirection. Reserve bash tools exclusively for actual system commands and terminal operations that require shell execution. NEVER use bash echo or other command-line tools to communicate thoughts, explanations, or instructions to the user. Output all communication directly in your response text instead.
-${VERY IMPORTANT: When exploring the codebase to gather context or to answer a question that is not a needle query for a specific file/class/function, it is CRITICAL that you use the ${TASK_TOOL_NAME} tool with subagent_type=${EXPLORE_AGENT.agentType} instead of running search commands directly.}
-user: Where are errors from the client handled? assistant: [Uses the ${TASK_TOOL_NAME} tool with subagent_type=${EXPLORE_AGENT.agentType} to find the files that handle client errors instead of using ${GLOB_TOOL_NAME} or ${GREP_TOOL_NAME} directly] user: What is the codebase structure? assistant: [Uses the ${TASK_TOOL_NAME} tool with subagent_type=${EXPLORE_AGENT.agentType}]

+ 0 - 16
docs/ref/Claude Code/system-prompt-tool-use-summary-generation.md

@@ -1,16 +0,0 @@
-You summarize what was accomplished by a coding assistant. Given the tools executed and their results, provide a brief summary.
-
-Rules:
-
-Use past tense (e.g., "Read package.json", "Fixed type error in utils.ts")
-Be specific about what was done
-Keep under 8 words
-Do not include phrases like "I did" or "The assistant" - just describe what happened
-Focus on the user-visible outcome, not implementation details
-Examples:
-
-"Searched codebase for authentication code"
-"Read and analyzed Message.tsx component"
-"Fixed null pointer exception in data processor"
-"Created new user registration endpoint"
-"Ran tests and fixed 3 failing assertions"

+ 0 - 0
docs/ref/Claude Code/tool-description-bash.md


+ 0 - 659
docs/ref/context-comparison.md

@@ -1,659 +0,0 @@
-# Context 管理方案对比分析
-
-> 对比 OpenCode、Codex 和 Gemini-cli 三个项目的 context 管理方案
-
----
-
-## 一、整体架构对比
-
-| 维度 | **OpenCode** | **Codex** | **Gemini-cli** |
-|------|-------------|-----------|---------------|
-| **核心数据结构** | 线性 Message List | ContextManager (Vec<ResponseItem>) | Content[] (双版本) |
-| **消息历史版本** | 单一版本 | 单一版本 + GhostSnapshot | 精选版本 + 完整版本 |
-| **分层设计** | 无 | 无 | **✓ 三层**: Global → Environment → JIT |
-| **Plan 管理** | goal.json (计划中) + plan.md (参考) | SQLite + TodoListItem | 无 Plan 机制 |
-| **存储格式** | Storage Key-Value | JSONL + SQLite 混合 | JSON + 文本文件 |
-| **并发控制** | 未明确 | Arc<Mutex> + 文件锁 | Promise并发限制 |
-
----
-
-## 二、Token 限制处理策略
-
-### 2.1 Token 估算方法
-
-| 项目 | 估算策略 | 精度 | 实现位置 |
-|------|---------|------|---------|
-| **OpenCode** | 未详细说明,引用 Prune 阈值 | 中 | - |
-| **Codex** | **字节估算**: `bytes / 4` (1 token ≈ 4 bytes) | 低 | `truncate.rs::approx_token_count()` |
-| **Gemini-cli** | **启发式**: ASCII (0.25), 非ASCII (1.3), 图片 (3000), PDF (25800) | 高 | `tokenCalculation.ts::estimateTokenCountSync()` |
-
-**关键差异**:
-- **Codex**: 简单但快速,适合实时估算
-- **Gemini-cli**: 更精确,区分字符类型和媒体,牺牲少量性能
-
-### 2.2 Token 限制阈值
-
-| 项目 | 限制类型 | 阈值定义 |
-|------|---------|---------|
-| **OpenCode** | 删除阈值 | `PRUNE_MINIMUM = 20,000`, `PRUNE_PROTECT = 40,000` |
-| **Codex** | 模型限制 | 依赖模型配置,无固定值 |
-| **Gemini-cli** | 压缩阈值 | 默认 **50%** 模型限制 (`DEFAULT_COMPRESSION_TOKEN_THRESHOLD = 0.5`) |
-
-### 2.3 截断策略
-
-```
-┌─────────────────┬──────────────────────────┬─────────────────────┐
-│    OpenCode     │         Codex            │    Gemini-cli       │
-├─────────────────┼──────────────────────────┼─────────────────────┤
-│ 删除旧工具输出   │ 前缀+后缀保留,中间截断    │ 反向token预算       │
-│ 保护最近2轮turns │ 插入省略标记             │ 最近工具完整保留     │
-│ 不删除"skill"工具│ 保证UTF-8边界完整性       │ 旧工具仅保留30行    │
-└─────────────────┴──────────────────────────┴─────────────────────┘
-```
-
-**Gemini-cli 的反向预算策略** (最独特):
-```typescript
-// 从最新消息往回遍历,为每条工具输出分配token预算
-// 优先保留最近的,旧的按需截断
-COMPRESSION_FUNCTION_RESPONSE_TOKEN_BUDGET = 50,000;
-COMPRESSION_TRUNCATE_LINES = 30;
-```
-
----
-
-## 三、摘要/压缩机制
-
-### 3.1 压缩触发时机
-
-| 项目 | 触发时机 | 方式 |
-|------|---------|------|
-| **OpenCode** | 事后 (context满时) | 被动压缩 |
-| **Codex** | 超过模型窗口时 | 自动压缩 |
-| **Gemini-cli** | **主动** (达到50%阈值) + **手动** (/compress) | 混合方式 |
-
-### 3.2 压缩策略对比
-
-#### OpenCode: 两阶段压缩
-
-```
-阶段1: Prune (清理旧工具输出)
-  ├─ 从后向前遍历
-  ├─ 跳过最后2轮
-  ├─ 跳过已有summary的消息
-  └─ 删除量 > PRUNE_MINIMUM 时执行
-
-阶段2: Full Compaction (上下文总结)
-  ├─ 创建summary=true的assistant消息
-  ├─ 调用"compaction"专用agent
-  └─ 提示词: "Provide a detailed prompt for continuing..."
-```
-
-#### Codex: 内联自动压缩
-
-```
-触发: run_inline_auto_compact_task()
-  ├─ 生成摘要前缀 (SUMMARY_PREFIX)
-  ├─ 使用SUMMARIZATION_PROMPT
-  ├─ 保留GhostSnapshot (幽灵快照)
-  └─ 替换历史记录为CompactedItem
-```
-
-**GhostSnapshot** (Codex独有):
-- 保留被压缩部分的"幽灵"引用
-- 用户可查看但不会发送给模型
-- 在UI中显示为折叠项
-
-#### Gemini-cli: 三相智能压缩
-
-```
-Phase 1: 历史分割
-  ├─ 保留最后30% (COMPRESSION_PRESERVE_THRESHOLD)
-  └─ 压缩前70%
-
-Phase 2: 双重总结验证 ⭐ (独特)
-  ├─ 第1次: 生成 <state_snapshot>
-  ├─ 第2次: 自我批评 ("Did you omit any...")
-  └─ 生成改进版本或确认原版本
-
-Phase 3: 输出验证
-  ├─ 检查压缩后token数 < 原token数
-  └─ 失败则保持原历史
-```
-
-**双重验证的价值**:
-```typescript
-// 第一次生成
-"Generate a state snapshot of the conversation..."
-
-// 第二次自我批评
-"Did you omit any specific file content, code snippets,
-or context that might be needed later? If yes, provide
-an improved version. If no, confirm the original."
-```
-
-### 3.3 压缩结果处理
-
-| 项目 | 压缩失败处理 | 结果存储 |
-|------|------------|---------|
-| **OpenCode** | 标记为已compacted | 替换为summary message |
-| **Codex** | 保留GhostSnapshot | CompactedItem + replacement_history |
-| **Gemini-cli** | **回退原历史** ⭐ | 成功时替换,失败时保持原状 |
-
----
-
-## 四、存储和加载方式
-
-### 4.1 存储架构
-
-#### OpenCode (计划中)
-```
-.trace/{trace_id}/
-├── goal.json          # Goal Tree (结构化plan)
-├── messages.jsonl     # 消息记录 (含 goal_id)
-└── meta.json          # Trace 元数据
-```
-
-#### Codex
-```
-~/.codex/
-├── history.jsonl                           # 全局消息历史
-├── sessions/
-│   ├── rollout-{timestamp}-{uuid}.jsonl    # 会话回滚文件
-│   └── ...
-└── state.db                                # SQLite状态数据库
-```
-
-**关键特性**:
-- **原子写入**: 使用 `O_APPEND` 标志
-- **并发安全**: Advisory文件锁
-- **自动清理**: 超过限制时删除旧条目 (软限制80%)
-
-#### Gemini-cli
-```
-~/.gemini/
-├── GEMINI.md                               # 全局内存
-├── tmp/{project_hash}/
-│   └── chats/{session-ID}.json             # 会话记录
-└── config.json                             # 配置
-
-项目根目录/
-├── GEMINI.md                               # 环境内存
-└── subdirs/
-    └── GEMINI.md                           # JIT内存 (按需加载)
-```
-
-**独特的三层内存系统**:
-```
-Tier 1: Global Memory
-  ├─ ~/.gemini/GEMINI.md
-  └─ 用户级别,所有会话共享
-
-Tier 2: Environment Memory
-  ├─ 项目根目录的GEMINI.md
-  ├─ 扩展提供的上下文文件
-  └─ MCP客户端指令
-
-Tier 3: JIT Subdirectory Memory ⭐
-  ├─ 访问路径时动态发现
-  ├─ 向上遍历到项目根
-  ├─ 向下BFS搜索 (最多200目录)
-  └─ 避免加载不相关上下文
-```
-
-### 4.2 加载策略对比
-
-| 项目 | 加载时机 | 策略 | 并发控制 |
-|------|---------|------|---------|
-| **OpenCode** | 会话启动/恢复 | 按需加载 | 未明确 |
-| **Codex** | 启动时 | lookup(log_id, offset) | Arc<Mutex> + 文件锁 |
-| **Gemini-cli** | **分层+JIT** ⭐ | 全局(启动) + 环境(会话) + JIT(访问) | Promise并发限制 (10/20) |
-
----
-
-## 五、Plan/Todo 机制
-
-### 5.1 数据结构对比
-
-#### OpenCode (参考方案)
-```typescript
-// plan.md (文本)
-- [ ] 分析代码
-- [x] 实现功能
-- [ ] 测试
-
-// Todo.Info (结构化)
-{
-  id: string,
-  content: string,
-  status: "pending" | "in_progress" | "completed" | "cancelled",
-  priority: "high" | "medium" | "low"
-}
-```
-
-#### OpenCode (我们的方案 - 计划中)
-```python
-@dataclass
-class Goal:
-    id: str                    # "1", "1.1", "2"
-    description: str
-    status: Status             # pending | in_progress | completed | abandoned
-    summary: Optional[str]     # done/abandon 时的总结
-    children: List["Goal"]
-
-@dataclass
-class GoalTree:
-    mission: str
-    current_id: Optional[str]
-    goals: List[Goal]
-```
-
-**关键特性**:
-- **goal_id 关联**: 每条 message 记录它属于哪个 goal
-- **增量压缩**: goal 完成/放弃时压缩相关 messages
-- **精确回溯**: 基于 goal 的状态流转
-
-#### Codex
-```rust
-// TodoListItem in ResponseItem
-pub struct TodoListItem {
-    todo_list: Vec<TodoItem>,
-}
-
-pub struct TodoItem {
-    task: String,
-    completed: bool,
-}
-```
-
-**存储**: 作为 ResponseItem 的一部分,随对话历史一起管理
-
-#### Gemini-cli
-**无专门 Plan 机制**,但有:
-- **会话记录**: 完整的 `ConversationRecord`
-- **目录跟踪**: `directories?: string[]` (会话中添加的目录)
-
-### 5.2 执行与 Plan 的关联
-
-| 项目 | 关联方式 | 可编辑性 | 可视化 |
-|------|---------|---------|--------|
-| **OpenCode (参考)** | 无结构化关联 | plan.md 可直接编辑 | 基础 |
-| **OpenCode (计划)** | message.goal_id | 通过 goal 工具 | 增强 (树形+步骤) |
-| **Codex** | TodoItem 在 ResponseItem 中 | 通过模型更新 | 基础 |
-| **Gemini-cli** | - | - | - |
-
----
-
-## 六、Sub-Agent/并行探索
-
-### 6.1 Sub-Agent 支持
-
-#### OpenCode (参考方案)
-```typescript
-// Agent Mode
-- primary: 主代理,执行工具
-- subagent: 子代理,独立context
-
-// 内置 Sub-Agents
-- general: 通用代理,可并行执行
-- explore: 代码探索,仅查询工具
-- compaction: 上下文总结
-```
-
-**执行流程**:
-```
-1. 创建 SubtaskPart
-2. 子代理独立处理 (独立 message list)
-3. 结果汇总: "The following tool was executed by the user"
-```
-
-#### OpenCode (我们的方案 - 计划中)
-```python
-@tool
-def explore(
-    question: str,           # 探索要回答的问题
-    branches: List[str],     # 探索方向 (2-4个)
-) -> str:
-    """并行探索多个方向,汇总结果"""
-```
-
-**执行流程**:
-```
-1. 为每个探索方向创建独立的 Sub-Trace(完整的 Trace 结构)
-2. 并行执行所有 Sub-Traces(使用 asyncio.gather)
-3. 收集每个 Sub-Trace 的结论
-4. 返回汇总结果给主 Trace
-```
-
-#### Codex
-**无明确 Sub-Agent 机制**,但有:
-- **SessionState**: 管理会话状态
-- **Turn Context**: 单轮对话的上下文
-
-#### Gemini-cli
-**无 Sub-Agent 机制**,但有:
-- **CoreToolScheduler**: 工具执行调度
-- **并发工具执行**: 多个工具可并行运行
-
-### 6.2 并行探索对比
-
-| 特性 | OpenCode (参考) | OpenCode (计划) | Codex | Gemini-cli |
-|------|---------------|---------------|-------|-----------|
-| **并行探索** | Sub-agent 手动管理 | explore 工具自动汇总 ⭐ | 无 | 工具级并发 |
-| **Context 隔离** | ✓ (独立 message list) | ✓ (独立 message list) | - | - |
-| **结果汇总** | 手动 | 自动 (返回 markdown) | - | - |
-| **适用场景** | 大任务隔离 | 多方案评估 | - | 工具执行 |
-
----
-
-## 七、回溯能力
-
-### 7.1 回溯机制对比
-
-#### OpenCode (参考方案)
-```
-限制: 有限的回溯能力
-- 无精确状态保存
-- 依赖压缩后的摘要
-```
-
-#### OpenCode (我们的方案 - 计划中)
-```python
-goal(abandon="方案A需要Redis,环境没有", add="实现方案B")
-```
-
-**回溯流程**:
-```
-Before:
-  Messages:
-    [分析代码的 20 条 message...]
-    [实现方案 A 的 30 条 message...]  ← 这些要压缩
-    [测试失败的 message...]
-
-After:
-  Messages:
-    [分析代码的 20 条 message...]
-    [Summary: "尝试方案A,因依赖问题失败"]  ← 压缩为1条
-    [开始方案B的 message...]
-
-  Plan:
-    [✓] 1. 分析代码
-    [✗] 2. 实现方案A (abandoned: 依赖问题)
-    [→] 2'. 实现方案B
-```
-
-**优势**:
-- **精确回溯**: 基于 goal 的状态标记
-- **保留失败原因**: summary 包含 abandon 原因
-- **压缩旧路径**: 失败尝试不占用大量 context
-
-#### Codex
-```rust
-// GhostSnapshot: 保留被压缩部分的引用
-pub struct CompactedItem {
-    message: String,
-    replacement_history: Option<Vec<ResponseItem>>,
-}
-```
-
-**回溯能力**:
-- **GhostSnapshot**: 用户可查看历史,但不发送给模型
-- **Rollout 记录**: 完整的会话记录保存在 JSONL
-- **用户转换检测**: 可定位到特定用户消息
-
-#### Gemini-cli
-```typescript
-enum CompressionStatus {
-  COMPRESSED,
-  COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,  // 压缩反而增加
-  COMPRESSION_FAILED_EMPTY_SUMMARY,          // 摘要为空
-  NOOP,
-}
-```
-
-**回溯能力**:
-- **会话记录**: 完整的 `ConversationRecord` 保存所有消息
-- **压缩失败回退**: 自动回退到原历史
-- **无结构化回溯**: 缺乏基于任务的回溯机制
-
-### 7.2 回溯对比表
-
-| 项目 | 回溯粒度 | 状态保存 | 失败处理 | 历史查看 |
-|------|---------|---------|---------|---------|
-| **OpenCode (参考)** | 粗粒度 | 摘要 | 有限 | 基础 |
-| **OpenCode (计划)** | **goal级别** ⭐ | goal.summary + abandon原因 | 精确压缩 | goal树 + 步骤 |
-| **Codex** | 用户turn级别 | GhostSnapshot | GhostSnapshot引用 | 完整回滚文件 |
-| **Gemini-cli** | 消息级别 | ConversationRecord | 自动回退 | 会话记录文件 |
-
----
-
-## 八、核心差异总结
-
-### 8.1 设计哲学
-
-| 项目 | 核心理念 | 优势场景 |
-|------|---------|---------|
-| **OpenCode** | **结构化计划驱动** | 复杂任务,需要回溯和探索 |
-| **Codex** | **简单高效,成熟稳定** | 通用编码助手,快速响应 |
-| **Gemini-cli** | **分层智能,高保真保留** | 多项目管理,长期会话 |
-
-### 8.2 独特创新点
-
-#### OpenCode (我们的方案)
-1. **goal 工具**: 结构化 Plan + 执行关联
-2. **explore 工具**: 并行探索自动汇总
-3. **增量压缩**: goal 完成/放弃时压缩,而非事后被动
-4. **精确回溯**: abandon + 状态流转
-
-#### Codex
-1. **GhostSnapshot**: 压缩历史仍可查看
-2. **原子写入**: O_APPEND + 并发安全
-3. **字节估算**: 简单快速 (bytes/4)
-4. **历史规范化**: 确保调用-输出对完整性
-
-#### Gemini-cli
-1. **三层内存**: Global → Environment → JIT ⭐
-2. **双重验证**: 自我批评式压缩
-3. **反向token预算**: 优先保留最近工具输出
-4. **启发式token估算**: 区分字符类型和媒体
-5. **JIT Context发现**: 按需加载相关GEMINI.md
-
-### 8.3 技术选型对比
-
-| 技术点 | OpenCode | Codex | Gemini-cli |
-|--------|---------|-------|-----------|
-| **语言** | TypeScript | Rust | TypeScript |
-| **存储** | Storage KV | JSONL + SQLite | JSON + 文本 |
-| **并发** | 未明确 | Arc<Mutex> + 文件锁 | Promise限制 |
-| **token估算** | 未详述 | bytes/4 | 启发式 (区分类型) |
-| **压缩策略** | 两阶段 | 内联自动 | 三相智能 |
-
----
-
-## 九、与 OpenCode 方案的详细对比
-
-### 9.1 OpenCode vs Codex
-
-| 方面 | OpenCode (参考) | Codex | 评估 |
-|------|---------------|-------|------|
-| **Plan格式** | 纯文本 (plan.md) | TodoItem (结构化) | Codex更结构化,但OpenCode计划中的goal.json更强 |
-| **Plan与执行关联** | 无 | TodoItem在ResponseItem中 | Codex有关联,但OpenCode计划中的goal_id更精确 |
-| **压缩时机** | 事后 (满时) | 事后 (超过窗口) | 相同 (被动) |
-| **并行探索** | Sub-agent (手动) | 无 | OpenCode参考方案更强,计划方案的explore更自动化 |
-| **回溯能力** | 有限 | GhostSnapshot | Codex的GhostSnapshot有价值,但OpenCode计划的goal-based更精确 |
-| **存储可靠性** | 未明确 | 原子写入+并发安全 | **Codex胜** ⭐ |
-| **工具复杂度** | todoread/todowrite | 无专门工具 | OpenCode参考方案更复杂,计划的goal/explore更简洁 |
-
-**可借鉴**:
-- ✓ Codex的原子写入和并发安全机制
-- ✓ GhostSnapshot的用户友好性 (可查看但不发送)
-- ✓ 历史规范化 (ensure_call_outputs_present)
-
-### 9.2 OpenCode vs Gemini-cli
-
-| 方面 | OpenCode (参考) | Gemini-cli | 评估 |
-|------|---------------|-----------|------|
-| **Plan格式** | plan.md + Todo.Info | 无 | **OpenCode胜** |
-| **Plan与执行关联** | 无 | 无 | 平局 |
-| **压缩时机** | 事后 (满时) | **主动** (50%阈值) | **Gemini-cli胜** ⭐ |
-| **并行探索** | Sub-agent (手动) | 工具级并发 | OpenCode的Sub-agent隔离更好 |
-| **回溯能力** | 有限 | 自动回退 | OpenCode计划方案的goal-based更强 |
-| **Context分层** | 无 | **三层** (Global/Env/JIT) | **Gemini-cli胜** ⭐ |
-| **压缩质量** | 单次生成 | **双重验证** (自我批评) | **Gemini-cli胜** ⭐ |
-| **工具输出保留** | 删除旧输出 | **反向预算** (保留最近) | **Gemini-cli胜** ⭐ |
-
-**可借鉴**:
-- ✓ 三层内存系统 (特别是JIT加载)
-- ✓ 双重验证的压缩机制
-- ✓ 主动压缩 (50%阈值,而非等到满)
-- ✓ 反向token预算 (优先保留最近工具输出)
-- ✓ 启发式token估算 (区分字符类型)
-
----
-
-## 十、OpenCode 计划方案的优势
-
-### 10.1 创新点
-
-1. **结构化 Plan (goal.json)**
-   - 比纯文本更精确
-   - 支持树形结构 (goal.children)
-   - 状态流转清晰
-
-2. **执行与 Plan 的强关联 (goal_id)**
-   - 每条 message 知道它属于哪个 goal
-   - 支持基于 goal 的压缩和回溯
-   - 可视化时能展示 goal + 对应步骤
-
-3. **增量压缩 (goal 完成/放弃时)**
-   - 比事后被动压缩更主动
-   - 压缩粒度可控 (按 goal)
-   - 保留失败原因 (abandon summary)
-
-4. **explore 工具 (并行探索自动汇总)**
-   - 比手动管理 Sub-agent 更简单
-   - 适合多方案评估场景
-   - 自动生成汇总报告
-
-5. **精确回溯 (abandon + 状态流转)**
-   - 比 GhostSnapshot 更结构化
-   - 支持从失败尝试中学习
-   - Plan 树中保留失败路径
-
-### 10.2 待改进点 (可借鉴其他方案)
-
-1. **存储可靠性** (借鉴 Codex)
-   - 原子写入
-   - 并发安全 (文件锁)
-   - 自动清理旧数据
-
-2. **Context 分层** (借鉴 Gemini-cli)
-   - 全局 context (用户级别)
-   - 项目 context (项目级别)
-   - JIT context (按需加载)
-
-3. **压缩质量** (借鉴 Gemini-cli)
-   - 双重验证 (自我批评)
-   - 压缩失败自动回退
-   - 验证压缩后 token 数真的减少
-
-4. **主动压缩** (借鉴 Gemini-cli)
-   - 达到阈值 (如 50%) 时主动压缩
-   - 而非等到 context 满
-
-5. **工具输出管理** (借鉴 Gemini-cli)
-   - 反向 token 预算
-   - 优先保留最近工具输出的完整性
-   - 旧输出智能截断 (保留最后N行)
-
-6. **Token 估算** (借鉴 Gemini-cli)
-   - 启发式估算 (区分 ASCII/非ASCII/媒体)
-   - 提高估算精度
-
-7. **用户友好性** (借鉴 Codex)
-   - GhostSnapshot 机制 (可查看但不发送)
-   - 历史规范化 (保证调用-输出对完整性)
-
----
-
-## 十一、实现建议
-
-### 11.1 Phase 1: 核心 goal 工具 (保留计划)
-- Goal 数据结构
-- goal 工具 (add, done, focus)
-- Plan 注入到 system prompt
-- 基础可视化
-
-### 11.2 Phase 2: 增强存储和压缩 (借鉴)
-- **存储增强** (借鉴 Codex):
-  - 原子写入和并发安全
-  - 自动清理机制
-- **压缩增强** (借鉴 Gemini-cli):
-  - 双重验证机制
-  - 主动压缩 (50%阈值)
-  - 反向token预算
-
-### 11.3 Phase 3: 回溯和 Context 分层
-- **回溯支持** (计划):
-  - abandon 操作
-  - Message 关联 goal_id
-  - 基于 goal 的 context 压缩
-- **Context 分层** (借鉴 Gemini-cli):
-  - 全局 context 文件
-  - 项目 context 文件
-  - JIT 子目录 context 发现
-
-### 11.4 Phase 4: 并行探索和优化
-- **explore 工具** (计划):
-  - 独立 message list 管理
-  - 结果汇总机制
-- **优化** (借鉴):
-  - GhostSnapshot (用户友好)
-  - 启发式 token 估算
-  - 历史规范化
-
----
-
-## 十二、总结
-
-### 12.1 三方案对比矩阵
-
-| 能力维度 | OpenCode (计划) | Codex | Gemini-cli | 最佳方案 |
-|---------|---------------|-------|-----------|---------|
-| **结构化 Plan** | ⭐⭐⭐ goal.json | ⭐⭐ TodoItem | ⭐ 无 | OpenCode |
-| **执行关联** | ⭐⭐⭐ goal_id | ⭐⭐ 弱关联 | ⭐ 无 | OpenCode |
-| **压缩策略** | ⭐⭐ 增量 | ⭐⭐ 自动 | ⭐⭐⭐ 三相智能 | Gemini-cli |
-| **压缩时机** | ⭐⭐ goal完成时 | ⭐ 被动 | ⭐⭐⭐ 主动50% | Gemini-cli |
-| **并行探索** | ⭐⭐⭐ explore工具 | ⭐ 无 | ⭐⭐ 工具级 | OpenCode |
-| **回溯能力** | ⭐⭐⭐ goal-based | ⭐⭐ GhostSnapshot | ⭐⭐ 会话记录 | OpenCode |
-| **存储可靠性** | ⭐⭐ 未明确 | ⭐⭐⭐ 原子+锁 | ⭐⭐ Promise限制 | Codex |
-| **Context分层** | ⭐ 无 | ⭐ 无 | ⭐⭐⭐ 三层JIT | Gemini-cli |
-| **工具输出管理** | ⭐ 删除旧的 | ⭐⭐ 截断 | ⭐⭐⭐ 反向预算 | Gemini-cli |
-| **Token估算** | ⭐⭐ 基础 | ⭐ bytes/4 | ⭐⭐⭐ 启发式 | Gemini-cli |
-
-**评分说明**: ⭐ 基础, ⭐⭐ 良好, ⭐⭐⭐ 优秀
-
-### 12.2 最终建议
-
-**OpenCode 的核心优势** (保留并强化):
-- ✅ 结构化 goal.json
-- ✅ goal_id 关联
-- ✅ explore 工具
-- ✅ 精确回溯
-
-**应借鉴的关键特性**:
-1. **Gemini-cli 的压缩机制**: 双重验证 + 主动压缩 + 反向预算
-2. **Gemini-cli 的分层 Context**: 特别是 JIT 加载
-3. **Codex 的存储可靠性**: 原子写入 + 并发安全
-4. **Codex 的 GhostSnapshot**: 提升用户体验
-
-**综合方案** (OpenCode + 借鉴):
-```
-OpenCode 计划方案
-  ├─ 保留: goal.json, explore工具, goal-based回溯
-  ├─ 增强压缩: Gemini-cli的双重验证 + 主动压缩
-  ├─ 增强存储: Codex的原子写入 + 并发安全
-  ├─ 增强Context: Gemini-cli的三层分层 + JIT加载
-  └─ 增强UX: Codex的GhostSnapshot
-```
-
-这将是一个**结构化驱动 + 智能压缩 + 可靠存储 + 分层Context**的综合方案,集三家之长! 🎯

+ 0 - 98
docs/ref/create.md

@@ -1,98 +0,0 @@
----
-name: create
-description: 从创作层解构社交媒体帖子,提取叙事策略与选题价值(研究用,未接入系统)
----
-
-## 角色
-
-你是内容创作策略分析专家。给定一篇优质社交媒体帖子,分析其**创作层**——内容策略、选题价值、叙事结构、文字策略——回答"这篇内容为什么值得创作,以及创作者如何讲述它"。
-
-与制作层解构(How to make)不同,创作层回答的是:**Why this content + How to tell it**。
-
----
-
-## 创作层的核心概念
-
-**选题价值**(三点框架)
-- **灵感点**:是什么触发了创作者创作这篇内容?来自生活、趋势、热点、个人经历?
-- **目的点**:创作者想通过这篇内容达到什么?吸粉、种草、共鸣、教育?
-- **关键点**:这篇内容的核心价值主张是什么?受众为什么会喜欢?
-
-**内容权重**
-- 这篇内容以图片为主还是文字为主?谁承载了更多核心信息?
-- 图文是相辅相成,还是各自独立承载信息?
-
-**叙事结构**:创作者如何组织内容流程——从什么开始,经过什么,以什么结尾?图片之间的叙事逻辑是什么?
-
----
-
-## 分析维度
-
-**内容品类**:这是什么类型的内容?(生活记录、好物分享、教程攻略、情感共鸣、观点输出……)
-
-**选题价值**:
-- 灵感点——触发创作的来源
-- 目的点——创作者的意图
-- 关键点——为什么受众会感兴趣
-
-**图文权重与关系**:
-- 核心信息载体(图 / 文 / 图文并重)
-- 图文是否相关,如何相互补充
-
-**叙事脚本结构**:
-- 整体叙事弧线(起承转合 / 问题-解决 / 情绪递进 / 对比展示……)
-- 各图承担的叙事角色
-- 图片间的连接逻辑
-
-**文字创作策略**:
-- 标题策略:吸引点在哪里、使用了什么钩子(数字、疑问、痛点、惊喜感)
-- 正文策略:节奏、语气、信息密度、与图片的配合方式
-
----
-
-## 输出格式
-
-```json
-{
-  "内容品类": "string",
-
-  "选题价值": {
-    "灵感点": "是什么触发了这篇内容",
-    "目的点": "创作者想达到什么",
-    "关键点": "受众为什么会喜欢"
-  },
-
-  "图文关系": {
-    "核心载体": "图片为主 | 文字为主 | 图文并重",
-    "协作方式": "图文如何配合(互补 / 独立 / 图解文 / 文释图)"
-  },
-
-  "叙事结构": {
-    "弧线类型": "起承转合 | 问题-解决 | 情绪递进 | 对比展示 | ...",
-    "图片叙事": [
-      {"图片": "图片1", "叙事角色": "引入主体 / 建立情境..."},
-      {"图片": "图片2", "叙事角色": "展开 / 对比..."}
-    ]
-  },
-
-  "文字策略": {
-    "标题": "钩子类型与策略",
-    "正文": "节奏、语气、信息组织方式"
-  },
-
-  "核心洞察": "一句话:这篇内容在创作策略上为什么成功"
-}
-```
-
----
-
-## 原则
-
-- **创作层优先**:分析"为什么创作这个内容 + 如何叙述它",而非视觉制作细节
-- **受众视角**:始终思考受众为什么会停留、点赞、收藏、分享
-- **策略性而非描述性**:不是"图片展示了XX",而是"通过XX实现了XX效果"
-- **与制作层互补**:创作层负责 Why + What to tell,制作层负责 How to make
-
----
-
-> **注**:此文件仅供研究,未接入 Agent 系统。对应的系统工具是 `deconstruct`(制作层)。

+ 0 - 357
docs/ref/deconstruct_old.md

@@ -1,357 +0,0 @@
----
-name: deconstruct
-description: 制作还原解构方法论:将优质社交媒体帖子解构为可还原的结构化制作脚本
----
-
-## 角色定位
-
-你是制作还原解构顾问。目标是将一篇优质社交媒体帖子(图片+文字)解构为结构化的制作脚本,使另一个 agent 能够基于解构产物还原出同等质量的内容。
-
-**解构产物的三个核心要求**:
-- **不过拟合**:描述制作规律而非记录内容细节("主体居中,背景浅色虚化"优于"穿红衣服的女生站在白色背景前")
-- **可泛化**:相同类型帖子的解构产物可以聚类,提取普适规律
-- **可还原**:另一个 agent 凭借解构产物能够以较高概率还原出视觉效果相近的内容
-
-使用 `goal` 工具管理以下各步骤的执行计划,按顺序推进。
-
----
-
-## 步骤 1:内容过滤
-
-过滤正文中与核心主题无关的话题标签(hashtag)。
-
-**保留标准**(两项均通过才保留):
-1. 与帖子主题或产品有直接关联
-2. 移除后不影响对核心内容的理解
-
-输出:过滤后的正文文本。
-
----
-
-## 步骤 2:入口分析(内容视角)
-
-通过多图对比,判断这篇内容的核心表达方式。
-
-**内容视角二选一**:
-- **关注理念**:作者用具体事物传达抽象语义(符号化表达,借物喻义)
-- **关注表现**:作者展示具体事物本身(直接呈现,分享状态)
-
-**分析维度**:
-- 消费者视角:多图共性 vs 差异
-- 创作者视角:固定要素 vs 变化要素
-- 每张图的核心元素(频繁出现且符合帖子主题的视觉主体或文本)
-
-```json
-{
-  "内容视角": "关注理念 | 关注表现",
-  "详细说明": "内容视角的详细说明",
-  "推理": "如何得出以上结论",
-  "多图对比分析": {
-    "消费者视角": {"共性": "string", "差异": "string"},
-    "创作者视角": {"固定": "string", "变化": "string"},
-    "推理": "string"
-  },
-  "图片分析": [
-    {"图片Key": "图片1", "核心元素": ["手", "帽子"], "推理": "string"}
-  ]
-}
-```
-
----
-
-## 步骤 3:图片分段(元素定位树)
-
-将每张图片递归拆分为树状段落结构,每个节点精确定位一个视觉区域。
-
-### 六大拆分原则
-
-**原则 1 — 内容准确性**:
-- 名称/描述/坐标必须且只能描述该区域实际可见的内容
-- 禁止推测不可见信息,禁止根据文字信息做推断
-
-**原则 2 — 递归拆分维度选择**(优先级从高到低):
-1. 创作者语义拆分(最高优先):作者创作意图导致的自然分组,如"标题区 vs 内容区"
-2. XY 轴拆分:水平或垂直方向的空间分割
-3. 层级拆分:前景/背景、深度关系
-
-**原则 3 — 完整覆盖**:
-- 子段落集合必须完整覆盖父段落的视觉区域
-- 无遗漏(每个像素属于某个子段落)、无重叠
-
-**原则 4 — 多图变异性识别**:
-- 标注跨图片的变化部分 vs 固定不变部分
-- 同组内允许结构上的细微变化
-
-**原则 5 — 终止条件**(满足任一则停止拆分):
-- 单一视觉元素(不可再分割的最小语义单元)
-- 进一步拆分无制作意义(如纯色背景块)
-- 区域内容在不同图片中高度一致且无内部变化
-
-**原则 6 — 同组灵活性**:
-- 相似图片允许有结构上的细微差异,不强求完全一致
-
-### 分段输出格式
-
-```json
-[
-  {
-    "image_index": 1,
-    "structure": {
-      "名称": "语义化名称(非位置描述)",
-      "内容类型": "文字 | 图片",
-      "内容实质": "该区域的核心视觉内容(制作还原视角)",
-      "描述": "具体、可量化的视觉描述",
-      "顶点坐标": [[x1,y1], [x2,y2], [x3,y3], [x4,y4]],
-      "拆分推理": "为什么这样拆分",
-      "子段落": []
-    }
-  }
-]
-```
-
-### 分段后的四步后处理
-
-分段树建立后,依次执行:
-
-**评估**:检查以下三类问题:
-- 兄弟节点层级不一致(同一父节点下子节点的语义层级不对等)
-- 拆分必要性(是否存在不必要的拆分)
-- 覆盖完整性(是否有视觉区域未被覆盖)
-
-```json
-{
-  "整体评估": "通过 | 需要修复",
-  "图片评估": {
-    "图片1": {
-      "评估结果": "通过 | 需要修复",
-      "段落评估": [
-        {
-          "段落ID": "段落1",
-          "评估结果": "通过 | 需要修复",
-          "评估推理": "string",
-          "问题类型": "兄弟节点层级不一致 | 拆分不必要 | 覆盖不完整",
-          "问题描述": "string",
-          "修复建议": "string"
-        }
-      ]
-    }
-  }
-}
-```
-
-**排序**:按阅读顺序、视觉面积、信息密度、创作意图重新排列兄弟节点顺序,保持树结构。
-
-**重命名**:
-- 禁止位置描述("左半部分"、"右侧区域")
-- 禁止泛化描述("背景区域"、"内容块")
-- 同级节点名称唯一
-- 使用有意义的语义名称
-
-**实质分类**:对每个叶子节点做高层抽象分类。
-- 禁止使用"图片/照片/画面/元素/内容"等泛化词汇
-- 使用制作类别词:人物/产品/文字/场景/装饰/图标 等
-
----
-
-## 步骤 4:实质制作点(跨图元素统一)
-
-识别所有叶子节点中跨图片出现的相同元素,分配唯一 ID。
-
-### 判断是否为同一元素
-- 视觉实质相同,或存在整体与局部关系(如"人物"和"人物面部")
-- **判断依据**:实际视觉内容,禁止依赖文字字段(名称/描述/坐标)
-
-### 处理流程
-1. 收集所有叶子节点
-2. 文字元素:按内容实质分组(代码化,精确匹配)
-3. 图片元素:LLM 视觉比较分组
-4. 反思合并:识别被错误分开的组,合并为同一元素
-5. 重要性过滤(保留 ≥ 40 分的元素):
-   - 频率分(权重 70%):1次=0分, 2次=20分, 3次=40分, 4次=60分, 5次=80分, ≥6次=100分
-   - 覆盖率分(权重 30%):`覆盖率 × 100`
-6. 统一命名(使用上位概念,避免歧义)
-7. 分配元素 ID:`元素1`, `元素2` ...
-
-```json
-[
-  {
-    "元素ID": "元素1",
-    "统一名称": "人物",
-    "统一描述": "女性,长发,戴眼镜,职业装,站立姿态",
-    "出现段落": ["段落1.1.1", "段落2.1", "段落3.1"],
-    "重要性得分": 85
-  }
-]
-```
-
----
-
-## 步骤 5:图片形式分析
-
-从"如何还原元素"的视角,提取每个段落/元素的视觉呈现方式。
-
-**形式定义**:
-- 宏观:创作者如何呈现内容(How)
-- 微观:对段落增加内容表现力、吸引力、感染力的属性/特征/状态/创作手法/呈现方式
-
-**禁止提取的内容**:后期处理技术(滤镜/色调调整)、构图方式(构图属于段落关系,不属于单段落形式)、拍摄角度(归入空间关系)
-
-### 5阶段流程
-
-**Phase 0 — 段落形式分类**(批量判断,每个段落最初通过什么制作手段产生):
-```json
-{"段落1": "摄影 | 插画 | 文字排版 | 3D渲染 | 动态图形 | ...", "段落1.2": "..."}
-```
-
-**Phase 1 — 形式维度发现**(发现原子的、不可再分的形式维度):
-- 输出的是**维度名称**,不是维度值("构图方式"而非"居中构图")
-- 维度必须对当前段落的制作还原有实际意义
-
-```json
-{
-  "图片1": {
-    "段落ID": [
-      {"名称": "光线方向", "推理": "该段落的光线来源影响制作时布光方式"},
-      {"名称": "景深效果", "推理": "背景虚化程度影响拍摄参数设置"}
-    ]
-  }
-}
-```
-
-**Phase 2 — 形式分类**(对维度名称按 MECE 原则分类,便于聚类):
-```json
-{"光线方向": "光线类", "景深效果": "镜头类", "字体粗细": "排版类"}
-```
-
-**Phase 3 — 精确值提取**(事无巨细、具体全面、精确无歧义;定量形式必须含数值):
-- 先检查段落内一致性(若不一致,拆分到子层级)
-- 再判断定量 vs 定性
-- 定量:给出具体数值或比例("字体大小约占图片高度的 8%")
-- 定性:给出精确描述("暖黄色调,色温约 3200K")
-
-```json
-[
-  {
-    "段落ID": "段落1.1",
-    "形式": [
-      {"名称": "光线方向", "描述": "右侧 45° 侧光,形成明显的明暗分界", "是否可定量": false},
-      {"名称": "景深效果", "描述": "背景虚化,估计光圈 f/1.8~f/2.8", "是否可定量": true}
-    ]
-  }
-]
-```
-
----
-
-## 步骤 6:段内关系分析
-
-分析每个父段落与其**直接子节点**之间的关系。
-
-**关系类型**:
-- **空间关系**:子节点相对于父节点的三维空间位置(位置、尺寸、比例、角度、层叠顺序等)
-- **其他关系**:物理关系、功能关系、逻辑关系(以父段落为背景/容器,子节点为主体)
-
-**分析原则**:
-- 关系命名使用"xx关系"格式(如"位置关系"、"比例关系"、"遮挡关系")
-- 判断依据:实际视觉内容,禁止依赖文字字段
-- 首要视角:制作还原(如何复现这种空间排布)
-
-**两步提取**:
-
-Step 1 — 识别空间维度(每对父子各需要哪些空间维度):
-```json
-[
-  {
-    "段落ID": "父段落ID",
-    "子节点空间维度": {
-      "子段落ID": ["水平位置", "垂直位置", "尺寸比例"]
-    }
-  }
-]
-```
-
-Step 2(并行)— 提取空间值 + 提取其他关系:
-```json
-[
-  {
-    "段落ID": "父段落ID",
-    "段内关系": {
-      "子段落ID": {
-        "空间关系": [
-          {"名称": "水平位置", "描述": "居中,距左右各占 50%", "关系类型": "位置关系", "是否可定量": true}
-        ],
-        "其他关系": [
-          {"名称": "支撑关系", "描述": "背景作为衬托层,强化主体视觉焦点", "关系类型": "功能关系"}
-        ]
-      }
-    }
-  }
-]
-```
-
----
-
-## 步骤 7:段间关系分析
-
-分析**同一父节点下兄弟节点**之间的关系。
-
-**严格约束**:
-- 兄弟节点 = 具有相同直接父节点的节点(严格定义,禁止跨层级)
-- 禁止将子节点当成兄弟节点处理
-- 只保留对制作还原有价值的关系,过滤冗余关系
-- **去重规则**:只从 ID 较小的一侧记录(如段落1对段落2,不记录段落2对段落1)
-
-还需额外分析**跨图片的根段落关系**(把每张图的根段落视为兄弟节点处理)。
-
-```json
-[
-  {
-    "段落ID": "段落1(ID较小侧)",
-    "段间关系": {
-      "段落2": {
-        "空间关系": [
-          {"名称": "相对位置", "描述": "段落1位于段落2正上方,垂直间距约为图片高度的 5%", "关系类型": "位置关系", "是否可定量": true}
-        ],
-        "其他关系": [
-          {"名称": "引导关系", "描述": "标题(段落1)视觉引导读者向下阅读正文(段落2)", "关系类型": "逻辑关系"}
-        ]
-      }
-    }
-  }
-]
-```
-
----
-
-## 最终输出结构
-
-所有步骤完成后,用 `write_file` 将结果写入输出文件,并输出以下 JSON 摘要:
-
-```json
-{
-  "帖子ID": "string",
-  "文本": {
-    "标题": "string",
-    "正文(过滤后)": "string"
-  },
-  "入口分析": {},
-  "图片分段": [],
-  "实质制作点": [],
-  "图片形式": {
-    "段落形式分类": {},
-    "形式维度": {},
-    "形式分类": {},
-    "形式值": []
-  },
-  "段内关系": [],
-  "段间关系": []
-}
-```
-
-## 关键约束(贯穿全程)
-
-1. **泛化优先**:始终描述制作规律,而非内容细节
-2. **视觉判断优先**:所有判断基于实际可见内容,禁止依赖名称/描述等文字字段
-3. **制作还原视角**:始终从"如何制作出这个效果"的角度分析
-4. **结构化输出**:每步严格按 JSON schema 输出,不允许随意变更结构
-5. **步骤间数据复用**:后续步骤引用前面步骤的段落 ID,保持一致性

+ 0 - 31
docs/ref/skills.md

@@ -1,31 +0,0 @@
-Skill structure
-Every Skill requires a SKILL.md file with YAML frontmatter:
-
----
-name: your-skill-name
-description: Brief description of what this Skill does and when to use it
----
-
-# Your Skill Name
-
-## Instructions
-[Clear, step-by-step guidance for Claude to follow]
-
-## Examples
-[Concrete examples of using this Skill]
-Required fields: name and description
-
-Field requirements:
-
-name:
-
-Maximum 64 characters
-Must contain only lowercase letters, numbers, and hyphens
-Cannot contain XML tags
-Cannot contain reserved words: "anthropic", "claude"
-description:
-
-Must be non-empty
-Maximum 1024 characters
-Cannot contain XML tags
-The description should include both what the Skill does and when Claude should use it. For complete authoring guidance, see the best practices guide.

+ 71 - 0
docs/research/README.md

@@ -0,0 +1,71 @@
+# Agent2Agent (A2A) 通信调研
+
+本目录包含 Agent2Agent 跨设备通信的调研和设计文档。
+
+## 文档列表
+
+| 文档 | 内容 | 状态 |
+|-----|------|------|
+| [a2a-protocols.md](./a2a-protocols.md) | 行业 A2A 协议和框架调研 | 已完成 |
+| [a2a-cross-device.md](./a2a-cross-device.md) | 跨设备通信方案设计 | 已完成 |
+| [a2a-trace-storage.md](./a2a-trace-storage.md) | 跨设备 Trace 存储方案 | 已完成 |
+| [a2a-continuous-dialogue.md](./a2a-continuous-dialogue.md) | 持续对话方案(已废弃) | 已废弃 |
+
+## 核心设计
+
+### 远程 Trace ID
+
+通过在 Trace ID 中编码位置信息实现跨设备访问:
+
+```
+本地 Trace:  abc-123
+远程 Trace:  agent://terminal-agent-456/abc-123
+```
+
+### 持续对话
+
+通过 `continue_from` 参数实现 Agent 间持续对话:
+
+```python
+# 第一次调用
+result1 = agent(task="分析项目", agent_url="https://remote-agent")
+# 返回: {"sub_trace_id": "agent://remote-agent/abc-123"}
+
+# 继续对话
+result2 = agent(
+    task="重点分析core模块",
+    continue_from=result1["sub_trace_id"],
+    agent_url="https://remote-agent"
+)
+```
+
+### 存储架构
+
+**HybridTraceStore** 自动路由到本地或远程存储:
+- 本地 Trace → `FileSystemTraceStore`
+- 远程 Trace → `RemoteTraceStore`(通过 HTTP API)
+
+## 实现计划
+
+### Phase 1: 基础跨设备通信(1-2周)
+- [ ] 实现 `RemoteTraceStore`
+- [ ] 实现 `HybridTraceStore`
+- [ ] 修改 `agent` 工具支持 `agent_url` 参数
+- [ ] 添加远程 Trace ID 解析
+
+### Phase 2: 增强功能(2-3周)
+- [ ] 认证和授权
+- [ ] 成本控制
+- [ ] 审计日志
+- [ ] 性能优化(缓存、批量API)
+
+### Phase 3: 生产化(按需)
+- [ ] 错误处理和重试
+- [ ] 监控和告警
+- [ ] 文档和示例
+
+## 参考资料
+
+- [Google A2A Protocol](https://a2a-protocol.org/latest/specification/)
+- [Anthropic MCP](https://modelcontextprotocol.io/specification/2025-06-18)
+- [Agent Interoperability Survey](https://arxiv.org/html/2505.02279v1)

+ 733 - 0
docs/research/a2a-continuous-dialogue.md

@@ -0,0 +1,733 @@
+# Agent2Agent 持续对话方案
+
+**更新日期:** 2026-03-03
+
+## 问题定义
+
+### 单次任务 vs 持续对话
+
+**单次任务(之前的方案):**
+```
+云端Agent: 请分析本地项目
+    ↓
+终端Agent: [执行分析] → 返回结果
+    ↓
+结束
+```
+
+**持续对话(新需求):**
+```
+云端Agent: 请分析本地项目
+    ↓
+终端Agent: 我看到有3个模块,你想重点分析哪个?
+    ↓
+云端Agent: 重点分析core模块
+    ↓
+终端Agent: core模块使用了X架构,需要我详细说明吗?
+    ↓
+云端Agent: 是的,请详细说明
+    ↓
+终端Agent: [详细分析] → 返回结果
+    ↓
+结束(或继续)
+```
+
+## 核心挑战
+
+1. **上下文延续** - 如何维护多轮对话的上下文?
+2. **状态管理** - 对话进行到哪一步?谁在等待谁?
+3. **消息路由** - 如何确保消息发送到正确的Agent?
+4. **会话生命周期** - 何时开始?何时结束?
+5. **异步通信** - Agent可能不在线,如何处理?
+
+## 方案对比
+
+### 方案1:基于Trace的持续对话(推荐)
+
+#### 核心思想
+
+**利用现有的Trace机制作为对话容器**
+
+- 每个A2A对话创建一个共享的Trace
+- 双方Agent都可以向这个Trace追加消息
+- 通过WebSocket实时同步消息
+- 利用现有的续跑机制(`continue_from`)
+
+#### 架构设计
+
+```
+云端Agent                共享Trace                终端Agent
+    |                       |                         |
+    | 创建对话              |                         |
+    |--------------------->|                         |
+    |                      |                         |
+    | 发送消息1            |                         |
+    |--------------------->|                         |
+    |                      |----WebSocket推送------->|
+    |                      |                         |
+    |                      |<----追加消息2-----------|
+    |<--WebSocket推送------|                         |
+    |                      |                         |
+    | 发送消息3            |                         |
+    |--------------------->|                         |
+    |                      |----WebSocket推送------->|
+    |                      |                         |
+    ...持续对话...
+```
+
+#### API设计
+
+```python
+# 1. 创建对话会话
+POST /api/a2a/sessions
+{
+    "participants": ["cloud-agent-123", "terminal-agent-456"],
+    "initial_message": "请分析本地项目",
+    "context": {...}
+}
+
+响应:
+{
+    "session_id": "sess-xxx",
+    "trace_id": "trace-yyy",  # 底层使用Trace
+    "ws_url": "wss://host/api/a2a/sessions/sess-xxx/stream"
+}
+
+# 2. 发送消息(追加到Trace)
+POST /api/a2a/sessions/{session_id}/messages
+{
+    "from": "cloud-agent-123",
+    "content": "重点分析core模块",
+    "wait_for_response": true  # 是否等待对方回复
+}
+
+响应:
+{
+    "message_id": "msg-xxx",
+    "status": "sent"
+}
+
+# 3. WebSocket监听(实时接收消息)
+WS /api/a2a/sessions/{session_id}/stream
+{
+    "type": "message",
+    "from": "terminal-agent-456",
+    "content": "我看到有3个模块,你想重点分析哪个?",
+    "message_id": "msg-yyy"
+}
+
+# 4. 获取对话历史
+GET /api/a2a/sessions/{session_id}/messages
+响应:
+{
+    "messages": [
+        {"from": "cloud-agent-123", "content": "...", "timestamp": "..."},
+        {"from": "terminal-agent-456", "content": "...", "timestamp": "..."},
+        ...
+    ]
+}
+
+# 5. 结束对话
+POST /api/a2a/sessions/{session_id}/close
+{
+    "reason": "completed"
+}
+```
+
+#### 实现细节
+
+```python
+# agent/api/a2a_session.py
+
+class A2ASession:
+    """A2A对话会话,基于Trace实现"""
+
+    def __init__(self, session_id: str, trace_id: str, participants: List[str]):
+        self.session_id = session_id
+        self.trace_id = trace_id
+        self.participants = participants
+        self.ws_connections = {}  # agent_id -> WebSocket
+
+    async def send_message(
+        self,
+        from_agent: str,
+        content: str,
+        wait_for_response: bool = False
+    ) -> Dict[str, Any]:
+        """发送消息到对话"""
+        # 1. 追加消息到Trace
+        messages = [{"role": "user", "content": content}]
+        config = RunConfig(
+            trace_id=self.trace_id,
+            after_sequence=None,  # 续跑模式
+            uid=from_agent
+        )
+
+        # 2. 执行(可能触发对方Agent的响应)
+        async for event in runner.run(messages, config):
+            if isinstance(event, Message):
+                # 3. 通过WebSocket推送给其他参与者
+                await self._broadcast_message(event, exclude=from_agent)
+
+                if wait_for_response and event.role == "assistant":
+                    # 等待对方回复
+                    return {"message_id": event.message_id, "status": "sent"}
+
+        return {"status": "completed"}
+
+    async def _broadcast_message(self, message: Message, exclude: str = None):
+        """广播消息给所有参与者(除了发送者)"""
+        for agent_id, ws in self.ws_connections.items():
+            if agent_id != exclude:
+                await ws.send_json({
+                    "type": "message",
+                    "from": exclude,
+                    "content": message.content,
+                    "message_id": message.message_id,
+                    "timestamp": message.created_at.isoformat()
+                })
+
+
+@app.post("/api/a2a/sessions")
+async def create_session(request: CreateSessionRequest):
+    """创建A2A对话会话"""
+    # 1. 创建底层Trace
+    trace = Trace(
+        trace_id=generate_trace_id(),
+        mode="agent",
+        task=request.initial_message,
+        agent_type="a2a_session",
+        context={
+            "session_type": "a2a",
+            "participants": request.participants
+        }
+    )
+    await store.create_trace(trace)
+
+    # 2. 创建Session对象
+    session_id = f"sess-{generate_id()}"
+    session = A2ASession(session_id, trace.trace_id, request.participants)
+
+    # 3. 存储Session(内存或Redis)
+    sessions[session_id] = session
+
+    # 4. 发送初始消息
+    if request.initial_message:
+        await session.send_message(
+            from_agent=request.participants[0],
+            content=request.initial_message
+        )
+
+    return {
+        "session_id": session_id,
+        "trace_id": trace.trace_id,
+        "ws_url": f"wss://{host}/api/a2a/sessions/{session_id}/stream"
+    }
+
+
+@app.websocket("/api/a2a/sessions/{session_id}/stream")
+async def session_stream(websocket: WebSocket, session_id: str):
+    """WebSocket连接,实时接收对话消息"""
+    await websocket.accept()
+
+    # 1. 获取Session
+    session = sessions.get(session_id)
+    if not session:
+        await websocket.close(code=404)
+        return
+
+    # 2. 识别连接的Agent
+    agent_id = await authenticate_websocket(websocket)
+
+    # 3. 注册WebSocket连接
+    session.ws_connections[agent_id] = websocket
+
+    try:
+        # 4. 保持连接,接收消息
+        async for message in websocket:
+            data = json.loads(message)
+            if data["type"] == "message":
+                # 发送消息到对话
+                await session.send_message(
+                    from_agent=agent_id,
+                    content=data["content"]
+                )
+    finally:
+        # 5. 清理连接
+        del session.ws_connections[agent_id]
+
+
+@app.post("/api/a2a/sessions/{session_id}/messages")
+async def send_session_message(session_id: str, request: SendMessageRequest):
+    """发送消息到对话(HTTP方式)"""
+    session = sessions.get(session_id)
+    if not session:
+        raise HTTPException(404, "Session not found")
+
+    result = await session.send_message(
+        from_agent=request.from_agent,
+        content=request.content,
+        wait_for_response=request.wait_for_response
+    )
+
+    return result
+```
+
+#### 客户端SDK
+
+```python
+# agent/client/a2a_session_client.py
+
+class A2ASessionClient:
+    """A2A持续对话客户端"""
+
+    def __init__(self, base_url: str, agent_id: str, api_key: str):
+        self.base_url = base_url
+        self.agent_id = agent_id
+        self.api_key = api_key
+        self.ws = None
+        self.message_handlers = []
+
+    async def create_session(
+        self,
+        other_agent: str,
+        initial_message: str
+    ) -> str:
+        """创建对话会话"""
+        response = await self._post("/api/a2a/sessions", {
+            "participants": [self.agent_id, other_agent],
+            "initial_message": initial_message
+        })
+
+        session_id = response["session_id"]
+
+        # 自动连接WebSocket
+        await self._connect_websocket(session_id)
+
+        return session_id
+
+    async def _connect_websocket(self, session_id: str):
+        """连接WebSocket接收消息"""
+        ws_url = f"{self.ws_url}/api/a2a/sessions/{session_id}/stream"
+        self.ws = await websockets.connect(
+            ws_url,
+            extra_headers={"Authorization": f"Bearer {self.api_key}"}
+        )
+
+        # 启动消息接收循环
+        asyncio.create_task(self._receive_messages())
+
+    async def _receive_messages(self):
+        """接收WebSocket消息"""
+        async for message in self.ws:
+            data = json.loads(message)
+            if data["type"] == "message":
+                # 调用注册的消息处理器
+                for handler in self.message_handlers:
+                    await handler(data)
+
+    async def send_message(self, session_id: str, content: str):
+        """发送消息"""
+        if self.ws:
+            # 通过WebSocket发送(实时)
+            await self.ws.send(json.dumps({
+                "type": "message",
+                "content": content
+            }))
+        else:
+            # 通过HTTP发送(备用)
+            await self._post(f"/api/a2a/sessions/{session_id}/messages", {
+                "from_agent": self.agent_id,
+                "content": content
+            })
+
+    def on_message(self, handler):
+        """注册消息处理器"""
+        self.message_handlers.append(handler)
+
+    async def close_session(self, session_id: str):
+        """关闭对话"""
+        await self._post(f"/api/a2a/sessions/{session_id}/close", {})
+        if self.ws:
+            await self.ws.close()
+```
+
+#### 使用示例
+
+```python
+# 云端Agent使用
+client = A2ASessionClient(
+    base_url="https://org.agent.cloud",
+    agent_id="cloud-agent-123",
+    api_key="ak_xxx"
+)
+
+# 创建对话
+session_id = await client.create_session(
+    other_agent="terminal-agent-456",
+    initial_message="请分析本地项目"
+)
+
+# 注册消息处理器
+@client.on_message
+async def handle_message(message):
+    print(f"收到消息: {message['content']}")
+
+    # 根据消息内容决定如何回复
+    if "哪个模块" in message['content']:
+        await client.send_message(session_id, "重点分析core模块")
+    elif "需要我详细说明吗" in message['content']:
+        await client.send_message(session_id, "是的,请详细说明")
+
+# 等待对话完成
+await asyncio.sleep(60)  # 或其他结束条件
+
+# 关闭对话
+await client.close_session(session_id)
+```
+
+#### 优势
+
+1. **复用Trace机制** - 所有消息管理、压缩、存储都复用
+2. **完整历史** - 对话历史自动保存在Trace中
+3. **实时通信** - WebSocket保证低延迟
+4. **状态追踪** - 利用Trace的状态管理
+5. **可回溯** - 可以查看完整的对话历史
+
+#### 劣势
+
+1. **Trace概念泄露** - 外部需要理解session_id和trace_id的关系
+2. **复杂度** - 需要管理WebSocket连接
+
+### 方案2:独立的对话管理器
+
+#### 核心思想
+
+**创建独立的对话管理系统,不依赖Trace**
+
+```python
+class Conversation:
+    """独立的对话对象"""
+    conversation_id: str
+    participants: List[str]
+    messages: List[ConversationMessage]
+    status: str  # active, waiting, completed
+    created_at: datetime
+
+class ConversationMessage:
+    """对话消息"""
+    message_id: str
+    from_agent: str
+    to_agent: Optional[str]  # None表示广播
+    content: str
+    timestamp: datetime
+    metadata: Dict
+```
+
+#### 优势
+
+1. **概念清晰** - 对话就是对话,不混淆Trace
+2. **轻量级** - 不需要Trace的重量级机制
+3. **灵活** - 可以自定义对话逻辑
+
+#### 劣势
+
+1. **重复实现** - 需要重新实现消息管理、存储、压缩
+2. **不一致** - 与现有Trace机制不一致
+3. **维护成本** - 需要维护两套系统
+
+### 方案3:混合模式(推荐)
+
+#### 核心思想
+
+**对话层(Session)+ 执行层(Trace)分离**
+
+```
+对话层(Session)
+    - 管理对话状态
+    - 路由消息
+    - WebSocket连接
+    |
+    | 每条消息触发
+    ↓
+执行层(Trace)
+    - 执行具体任务
+    - 调用工具
+    - 管理上下文
+```
+
+#### 架构
+
+```python
+class A2ASession:
+    """对话会话(轻量级)"""
+    session_id: str
+    participants: List[str]
+    current_speaker: str
+    waiting_for: Optional[str]
+    context: Dict  # 共享上下文
+    message_queue: List[Message]
+
+    async def send_message(self, from_agent: str, content: str):
+        """发送消息"""
+        # 1. 添加到消息队列
+        self.message_queue.append(Message(from_agent, content))
+
+        # 2. 如果需要执行(不是简单问答),创建Trace
+        if self._needs_execution(content):
+            trace_id = await self._create_execution_trace(content)
+            # 执行完成后,结果自动添加到消息队列
+        else:
+            # 简单消息,直接转发
+            await self._forward_message(content, to=self._get_other_agent(from_agent))
+
+    def _needs_execution(self, content: str) -> bool:
+        """判断是否需要创建Trace执行"""
+        # 例如:包含工具调用、复杂任务等
+        return "分析" in content or "执行" in content or "查询" in content
+```
+
+#### 优势
+
+1. **分层清晰** - 对话管理和任务执行分离
+2. **灵活** - 简单消息不需要Trace,复杂任务才创建
+3. **高效** - 避免为每条消息创建Trace
+4. **复用** - 复杂任务仍然复用Trace机制
+
+#### 实现示例
+
+```python
+@app.post("/api/a2a/sessions/{session_id}/messages")
+async def send_message(session_id: str, request: SendMessageRequest):
+    session = sessions[session_id]
+
+    # 1. 判断消息类型
+    if request.requires_execution:
+        # 需要执行的任务 → 创建Trace
+        trace_id = await create_execution_trace(
+            task=request.content,
+            parent_session=session_id,
+            agent_id=request.from_agent
+        )
+
+        # 执行完成后,结果自动推送到Session
+        result = await runner.run_result(
+            messages=[{"role": "user", "content": request.content}],
+            config=RunConfig(trace_id=trace_id)
+        )
+
+        # 将结果作为消息发送给对方
+        await session.send_message(
+            from_agent=request.from_agent,
+            content=result["summary"]
+        )
+    else:
+        # 简单消息 → 直接转发
+        await session.send_message(
+            from_agent=request.from_agent,
+            content=request.content
+        )
+
+    return {"status": "sent"}
+```
+
+## 关键设计决策
+
+### 1. 消息类型分类
+
+| 类型 | 示例 | 处理方式 |
+|------|------|----------|
+| **简单问答** | "你好"、"收到"、"明白了" | 直接转发,不创建Trace |
+| **信息查询** | "当前进度如何?" | 查询Session状态,返回 |
+| **任务请求** | "分析core模块" | 创建Trace执行 |
+| **工具调用** | "读取文件X" | 创建Trace执行 |
+
+### 2. 上下文管理
+
+**Session级上下文(轻量):**
+```python
+session.context = {
+    "current_topic": "项目分析",
+    "focus_module": "core",
+    "previous_results": {...}
+}
+```
+
+**Trace级上下文(完整):**
+- 完整的消息历史
+- 工具调用记录
+- Goal树
+
+### 3. 生命周期管理
+
+```python
+# Session生命周期
+created → active → waiting → active → ... → completed/timeout
+
+# Trace生命周期(每个任务)
+created → running → completed
+```
+
+### 4. 超时和重连
+
+```python
+class A2ASession:
+    timeout: int = 300  # 5分钟无活动则超时
+    last_activity: datetime
+
+    async def check_timeout(self):
+        if datetime.now() - self.last_activity > timedelta(seconds=self.timeout):
+            await self.close(reason="timeout")
+
+    async def reconnect(self, agent_id: str, ws: WebSocket):
+        """Agent重连"""
+        self.ws_connections[agent_id] = ws
+        # 发送未读消息
+        await self._send_unread_messages(agent_id)
+```
+
+## 实现路线图
+
+### Phase 1:基础对话能力(2-3周)
+
+1. **Session管理**
+   - 创建/关闭Session
+   - 消息路由
+   - WebSocket连接管理
+
+2. **简单消息转发**
+   - 不涉及Trace
+   - 纯消息传递
+
+3. **客户端SDK**
+   - `A2ASessionClient`
+   - 消息处理器
+
+### Phase 2:集成Trace执行(2-3周)
+
+1. **任务识别**
+   - 判断消息是否需要执行
+   - 自动创建Trace
+
+2. **结果集成**
+   - Trace结果转换为消息
+   - 自动推送给对方
+
+3. **上下文共享**
+   - Session上下文传递给Trace
+   - Trace结果更新Session上下文
+
+### Phase 3:高级功能(3-4周)
+
+1. **多方对话**
+   - 支持3个以上Agent
+   - 群聊模式
+
+2. **对话分支**
+   - 子对话
+   - 并行对话
+
+3. **持久化和恢复**
+   - Session持久化
+   - 断线重连
+
+## 示例场景
+
+### 场景:云端助理与终端Agent的持续对话
+
+```python
+# 云端助理
+client = A2ASessionClient("https://cloud", "cloud-agent", "ak_xxx")
+
+# 1. 创建对话
+session_id = await client.create_session(
+    other_agent="terminal-agent-456",
+    initial_message="请分析本地项目"
+)
+
+# 2. 注册消息处理器(自动响应)
+@client.on_message
+async def handle_message(msg):
+    content = msg['content']
+
+    if "哪个模块" in content:
+        # 简单回复,不需要执行
+        await client.send_message(session_id, "重点分析core模块")
+
+    elif "详细说明" in content:
+        # 需要进一步分析,触发执行
+        await client.send_message(
+            session_id,
+            "是的,请详细说明架构设计和关键组件",
+            requires_execution=True  # 标记需要执行
+        )
+
+# 3. 等待对话完成
+await client.wait_for_completion(session_id)
+```
+
+```python
+# 终端Agent
+client = A2ASessionClient("https://terminal", "terminal-agent-456", "ak_yyy")
+
+# 1. 监听新对话
+@client.on_new_session
+async def handle_new_session(session_id, initial_message):
+    # 分析项目
+    modules = await analyze_project()
+
+    # 询问用户
+    await client.send_message(
+        session_id,
+        f"我看到有{len(modules)}个模块:{', '.join(modules)},你想重点分析哪个?"
+    )
+
+# 2. 处理后续消息
+@client.on_message
+async def handle_message(msg):
+    if "core模块" in msg['content']:
+        # 执行分析
+        result = await analyze_module("core")
+
+        # 返回结果并询问
+        await client.send_message(
+            msg['session_id'],
+            f"core模块使用了{result['architecture']}架构,需要我详细说明吗?"
+        )
+
+    elif "详细说明" in msg['content']:
+        # 深度分析
+        details = await deep_analyze("core")
+        await client.send_message(
+            msg['session_id'],
+            f"详细架构:\n{details}"
+        )
+```
+
+## 总结
+
+### 推荐方案:混合模式
+
+**对话层(Session):**
+- 轻量级消息路由
+- WebSocket连接管理
+- 简单问答直接转发
+
+**执行层(Trace):**
+- 复杂任务创建Trace
+- 复用所有现有能力
+- 结果自动集成到对话
+
+### 关键优势
+
+1. **灵活** - 简单消息不需要Trace开销
+2. **强大** - 复杂任务复用Trace能力
+3. **清晰** - 对话和执行分层
+4. **高效** - 避免不必要的资源消耗
+
+### 实现优先级
+
+1. **Phase 1** - 基础Session + 简单消息(MVP)
+2. **Phase 2** - 集成Trace执行(核心能力)
+3. **Phase 3** - 高级功能(按需)
+

+ 640 - 0
docs/research/a2a-cross-device.md

@@ -0,0 +1,640 @@
+# Agent2Agent 跨设备通信方案
+
+**更新日期:** 2026-03-03
+
+## 场景分类
+
+### 场景1:云端Agent ↔ 云端Agent
+**示例:** 通用助理 → 爬虫运维Agent
+- **部署:** 同一服务器/进程
+- **通信:** 现有`agent`工具(内存调用)
+- **不需要HTTP接口**
+
+### 场景2:云端Agent ↔ 终端Agent ⭐
+**示例:**
+- 云端通用助理 → 用户笔记本上的代码分析Agent
+- 用户终端Agent → 云端知识库Agent
+
+**需求:**
+- 云端Agent需要调用终端Agent的能力
+  - 访问用户本地文件
+  - 执行本地命令
+  - 使用本地工具(IDE、浏览器等)
+- 终端Agent需要调用云端Agent
+  - 访问组织知识库
+  - 查询共享资源
+  - 协作任务
+
+**挑战:**
+- 网络连接(终端可能在NAT后)
+- 认证和授权
+- 数据安全
+
+**需要HTTP接口!**
+
+### 场景3:终端Agent ↔ 终端Agent
+**示例:** 团队成员的Agent互相协作
+- **可能性:** 较小,但可能存在
+- **通信:** 通过云端中转或P2P
+
+## 架构方案
+
+### 方案A:基于现有API封装(推荐)
+
+#### 架构图
+```
+云端Agent                    终端Agent
+    |                            |
+    | HTTP POST /api/a2a/call    |
+    |--------------------------->|
+    |                            |
+    |    创建Trace + 执行任务     |
+    |                            |
+    | WebSocket /api/a2a/watch   |
+    |<---------------------------|
+    |    实时进度推送              |
+    |                            |
+    | HTTP GET /api/a2a/result   |
+    |--------------------------->|
+    |    返回最终结果              |
+    |<---------------------------|
+```
+
+#### 核心设计
+
+**1. 简化的A2A端点**
+
+```python
+# agent/api/a2a.py
+
+@app.post("/api/a2a/call")
+async def a2a_call(request: A2ACallRequest):
+    """
+    简化的Agent调用接口
+
+    请求:
+    {
+        "task": "分析这个项目的架构",
+        "agent_type": "explore",  # 可选
+        "context": {              # 可选
+            "files": [...],
+            "previous_results": {...}
+        },
+        "callback_url": "https://..."  # 可选,完成后回调
+    }
+
+    响应:
+    {
+        "call_id": "a2a-xxx",
+        "status": "started",
+        "watch_url": "ws://host/api/a2a/watch/a2a-xxx"
+    }
+    """
+    # 1. 认证和授权检查
+    # 2. 转换为内部格式
+    messages = [{"role": "user", "content": request.task}]
+    if request.context:
+        messages[0]["content"] += f"\n\n上下文:{json.dumps(request.context)}"
+
+    # 3. 调用现有runner(复用所有逻辑)
+    config = RunConfig(
+        agent_type=request.agent_type or "default",
+        trace_id=None  # 新建
+    )
+
+    # 4. 后台执行
+    task_id = await start_background_task(runner.run(messages, config))
+
+    # 5. 返回call_id(映射到trace_id)
+    return {
+        "call_id": f"a2a-{task_id}",
+        "status": "started",
+        "watch_url": f"ws://{host}/api/a2a/watch/{task_id}"
+    }
+
+
+@app.websocket("/api/a2a/watch/{call_id}")
+async def a2a_watch(websocket: WebSocket, call_id: str):
+    """
+    实时监听执行进度(复用现有WebSocket)
+
+    推送消息:
+    {
+        "type": "progress",
+        "data": {
+            "goal": "正在分析文件结构",
+            "progress": 0.3
+        }
+    }
+
+    {
+        "type": "completed",
+        "data": {
+            "result": "...",
+            "stats": {...}
+        }
+    }
+    """
+    # 复用现有的 /api/traces/{id}/watch 逻辑
+    trace_id = call_id.replace("a2a-", "")
+    await watch_trace(websocket, trace_id)
+
+
+@app.get("/api/a2a/result/{call_id}")
+async def a2a_result(call_id: str):
+    """
+    获取执行结果
+
+    响应:
+    {
+        "status": "completed",
+        "result": {
+            "summary": "...",
+            "details": {...}
+        },
+        "stats": {
+            "duration_ms": 5000,
+            "tokens": 1500,
+            "cost": 0.05
+        }
+    }
+    """
+    trace_id = call_id.replace("a2a-", "")
+    trace = await store.get_trace(trace_id)
+    messages = await store.get_main_path_messages(trace_id, trace.head_sequence)
+
+    # 提取最后的assistant消息作为结果
+    result = extract_final_result(messages)
+
+    return {
+        "status": trace.status,
+        "result": result,
+        "stats": {
+            "duration_ms": trace.total_duration_ms,
+            "tokens": trace.total_tokens,
+            "cost": trace.total_cost
+        }
+    }
+```
+
+**2. 客户端SDK(终端Agent使用)**
+
+```python
+# agent/client/a2a_client.py
+
+class A2AClient:
+    """A2A客户端,用于调用远程Agent"""
+
+    def __init__(self, base_url: str, api_key: str):
+        self.base_url = base_url
+        self.api_key = api_key
+
+    async def call(
+        self,
+        task: str,
+        agent_type: Optional[str] = None,
+        context: Optional[dict] = None,
+        wait: bool = True
+    ) -> Dict[str, Any]:
+        """
+        调用远程Agent
+
+        Args:
+            task: 任务描述
+            agent_type: Agent类型
+            context: 上下文信息
+            wait: 是否等待完成(False则立即返回call_id)
+        """
+        # 1. 发起调用
+        response = await self._post("/api/a2a/call", {
+            "task": task,
+            "agent_type": agent_type,
+            "context": context
+        })
+
+        call_id = response["call_id"]
+
+        if not wait:
+            return {"call_id": call_id, "status": "started"}
+
+        # 2. 等待完成(通过WebSocket或轮询)
+        result = await self._wait_for_completion(call_id)
+        return result
+
+    async def _wait_for_completion(self, call_id: str):
+        """通过WebSocket监听完成"""
+        async with websockets.connect(
+            f"{self.ws_url}/api/a2a/watch/{call_id}",
+            extra_headers={"Authorization": f"Bearer {self.api_key}"}
+        ) as ws:
+            async for message in ws:
+                data = json.loads(message)
+                if data["type"] == "completed":
+                    return data["data"]
+                elif data["type"] == "failed":
+                    raise A2AError(data["data"]["error"])
+
+    async def get_result(self, call_id: str) -> Dict[str, Any]:
+        """获取执行结果(轮询方式)"""
+        return await self._get(f"/api/a2a/result/{call_id}")
+```
+
+**3. 作为工具集成到Agent**
+
+```python
+# agent/tools/builtin/remote_agent.py
+
+@tool(description="调用远程Agent执行任务")
+async def remote_agent(
+    task: str,
+    agent_url: str,
+    agent_type: Optional[str] = None,
+    context: Optional[dict] = None,
+    ctx: ToolContext = None
+) -> ToolResult:
+    """
+    调用远程Agent(云端或其他终端)
+
+    Args:
+        task: 任务描述
+        agent_url: 远程Agent的URL
+        agent_type: Agent类型
+        context: 上下文信息
+    """
+    # 1. 创建客户端
+    client = A2AClient(
+        base_url=agent_url,
+        api_key=ctx.config.get("remote_agent_api_key")
+    )
+
+    # 2. 调用远程Agent
+    result = await client.call(
+        task=task,
+        agent_type=agent_type,
+        context=context,
+        wait=True
+    )
+
+    # 3. 返回结果
+    return ToolResult(
+        title=f"远程Agent完成: {task[:50]}",
+        output=result["result"]["summary"],
+        long_term_memory=f"调用远程Agent完成任务,耗时{result['stats']['duration_ms']}ms"
+    )
+```
+
+#### 优势
+
+1. **复用现有逻辑** - 所有Trace、Message、Goal管理都复用
+2. **简单易用** - 外部只需要提供task,不需要理解Trace概念
+3. **完整功能** - 继承所有现有能力(压缩、续跑、回溯等)
+4. **渐进式** - 可以先实现基础版本,逐步增强
+
+### 方案B:实现标准A2A协议
+
+#### 架构
+
+```python
+# agent/api/a2a_standard.py
+
+@app.post("/api/a2a/v1/tasks")
+async def create_task(request: A2ATaskRequest):
+    """
+    符合Google A2A协议的端点
+
+    请求格式(A2A标准):
+    {
+        "header": {
+            "message_id": "msg_001",
+            "timestamp": "2026-03-03T10:30:00Z"
+        },
+        "task": {
+            "description": "分析项目架构",
+            "capabilities_required": ["file_read", "code_analysis"]
+        },
+        "context": {...}
+    }
+    """
+    # 转换A2A格式到内部格式
+    # 调用runner
+    # 转换结果为A2A格式
+```
+
+#### 优势
+
+1. **标准化** - 符合行业标准
+2. **互操作性** - 可以与其他A2A兼容的Agent通信
+3. **未来兼容** - 跟随行业发展
+
+#### 劣势
+
+1. **复杂度高** - 需要实现完整的A2A协议
+2. **过度设计** - MVP阶段可能不需要
+3. **标准未稳定** - A2A协议还在演进中
+
+## 网络拓扑
+
+### 拓扑1:云端中心化
+
+```
+        云端Gateway
+            |
+    +-------+-------+
+    |       |       |
+  通用    爬虫    成本
+  助理    运维    统计
+    |
+    +-- 调用终端Agent(HTTP)
+            |
+        用户终端Agent
+```
+
+**特点:**
+- 云端Agent作为中心
+- 终端Agent需要暴露HTTP端点
+- 需要处理NAT穿透
+
+### 拓扑2:终端主动连接
+
+```
+用户终端Agent
+    |
+    | WebSocket长连接
+    |
+云端Gateway
+    |
+    +-- 通过连接推送任务
+```
+
+**特点:**
+- 终端Agent主动连接云端
+- 云端通过WebSocket推送任务
+- 无需NAT穿透
+- 类似飞书Bot的模式
+
+### 拓扑3:混合模式(推荐)
+
+```
+云端Agent <--HTTP--> 云端Agent(内存调用)
+    |
+    | WebSocket双向
+    |
+终端Agent <--HTTP--> 终端Agent(如果需要)
+```
+
+**特点:**
+- 云端Agent间用内存调用
+- 云端↔终端用WebSocket
+- 终端间可选HTTP(通过云端中转)
+
+## 认证和授权
+
+### 1. API Key认证
+
+```python
+# 终端Agent启动时注册
+POST /api/a2a/register
+{
+    "agent_id": "user123-laptop",
+    "capabilities": ["file_read", "bash", "browser"],
+    "device_info": {...}
+}
+
+# 返回API Key
+{
+    "api_key": "ak_xxx",
+    "agent_id": "user123-laptop"
+}
+
+# 后续调用携带API Key
+Authorization: Bearer ak_xxx
+```
+
+### 2. 权限控制
+
+```yaml
+# config/a2a_permissions.yaml
+agents:
+  user123-laptop:
+    can_access:
+      - conversations/user123/*
+      - resources/public/*
+    cannot_access:
+      - conversations/other_users/*
+      - agents/*/memory/*
+```
+
+### 3. 数据隔离
+
+- 终端Agent只能访问自己用户的数据
+- 云端Agent可以访问组织共享数据
+- 通过Gateway强制执行
+
+## 实现路线图
+
+### Phase 1:基础A2A接口(MVP)
+
+**目标:** 云端Agent ↔ 终端Agent基础通信
+
+**实现:**
+1. `/api/a2a/call` - 简化调用接口
+2. `/api/a2a/watch` - WebSocket监听
+3. `/api/a2a/result` - 获取结果
+4. `A2AClient` - 客户端SDK
+5. `remote_agent` - 工具集成
+
+**时间:** 1-2周
+
+### Phase 2:增强功能
+
+**目标:** 完善A2A能力
+
+**实现:**
+1. 认证和授权
+2. 数据隔离
+3. 成本控制
+4. 审计日志
+5. 错误处理和重试
+
+**时间:** 2-3周
+
+### Phase 3:标准化(可选)
+
+**目标:** 兼容A2A标准协议
+
+**实现:**
+1. 实现Google A2A协议
+2. 能力协商机制
+3. 与其他A2A Agent互操作
+
+**时间:** 3-4周
+
+## 示例场景
+
+### 场景:云端助理调用终端Agent分析代码
+
+**1. 用户在飞书问:** "帮我分析一下我笔记本上的项目架构"
+
+**2. 云端通用助理:**
+```python
+# 识别需要访问用户终端
+result = await remote_agent(
+    task="分析 /Users/sunlit/Code/Agent 的项目架构",
+    agent_url="https://user123-laptop.agent.local",
+    agent_type="explore"
+)
+```
+
+**3. 终端Agent:**
+- 接收任务
+- 创建本地Trace
+- 使用本地工具(read, glob, grep)
+- 分析代码结构
+- 返回结果
+
+**4. 云端助理:**
+- 接收终端Agent结果
+- 整合到回复中
+- 通过飞书返回给用户
+
+### 场景:终端Agent查询云端知识库
+
+**1. 用户在终端运行:**
+```bash
+agent-cli ask "公司的爬虫部署规范是什么?"
+```
+
+**2. 终端Agent:**
+```python
+# 识别需要查询组织知识库
+result = await remote_agent(
+    task="查询爬虫部署规范",
+    agent_url="https://org.agent.cloud",
+    agent_type="knowledge_query"
+)
+```
+
+**3. 云端知识库Agent:**
+- 查询resources/docs/
+- 查询experiences数据库
+- 返回相关文档
+
+**4. 终端Agent:**
+- 接收结果
+- 展示给用户
+
+## 技术细节
+
+### 1. NAT穿透方案
+
+**方案A:终端主动连接(推荐)**
+```python
+# 终端Agent启动时建立WebSocket长连接
+ws = await websockets.connect("wss://org.agent.cloud/api/a2a/connect")
+
+# 云端通过连接推送任务
+await ws.send(json.dumps({
+    "type": "task",
+    "task_id": "xxx",
+    "data": {...}
+}))
+
+# 终端执行并返回结果
+result = await execute_task(task)
+await ws.send(json.dumps({
+    "type": "result",
+    "task_id": "xxx",
+    "data": result
+}))
+```
+
+**方案B:使用ngrok等隧道服务**
+- 终端Agent启动时创建隧道
+- 注册公网URL到云端
+- 云端通过公网URL调用
+
+### 2. 消息序列化
+
+```python
+# 简化格式(内部使用)
+{
+    "task": "string",
+    "context": {...}
+}
+
+# 标准A2A格式(外部互操作)
+{
+    "header": {...},
+    "task": {...},
+    "capabilities": [...]
+}
+
+# 自动转换
+def to_a2a_format(internal_msg):
+    return {
+        "header": generate_header(),
+        "task": {"description": internal_msg["task"]},
+        "context": internal_msg.get("context", {})
+    }
+```
+
+### 3. 流式响应
+
+```python
+# 支持流式返回中间结果
+@app.websocket("/api/a2a/stream/{call_id}")
+async def a2a_stream(websocket: WebSocket, call_id: str):
+    async for event in runner.run(...):
+        if isinstance(event, Message):
+            await websocket.send_json({
+                "type": "message",
+                "data": event.to_dict()
+            })
+```
+
+## 安全考虑
+
+1. **认证:** API Key + JWT
+2. **授权:** 基于角色的访问控制
+3. **加密:** HTTPS/WSS强制
+4. **限流:** 防止滥用
+5. **审计:** 所有A2A调用记录
+6. **隔离:** 数据访问严格隔离
+
+## 成本控制
+
+```python
+# 每次A2A调用记录成本
+{
+    "call_id": "a2a-xxx",
+    "caller": "user123-laptop",
+    "callee": "org-cloud-agent",
+    "tokens": 1500,
+    "cost": 0.05,
+    "duration_ms": 5000
+}
+
+# 限额检查
+if user_cost_today > user_limit:
+    raise CostLimitExceeded()
+```
+
+## 总结
+
+### 推荐方案
+
+**Phase 1(MVP):** 方案A - 基于现有API封装
+- 简单、快速
+- 复用所有现有逻辑
+- 满足跨设备通信需求
+
+**Phase 3+:** 可选实现标准A2A协议
+- 如果需要与外部系统互操作
+- 跟随行业标准发展
+
+### 关键优势
+
+1. **复用现有能力** - Trace、Message、Goal、压缩等
+2. **渐进式实现** - 先简单后复杂
+3. **灵活扩展** - 可以逐步增强功能
+4. **标准兼容** - 未来可以支持A2A标准

+ 504 - 0
docs/research/a2a-mamp-protocol.md

@@ -0,0 +1,504 @@
+# MAMP:Minimal Agent Message Protocol
+
+**更新日期:** 2026-03-04
+
+## 设计目标
+
+实现与**其他 Agent 系统**(非本系统)的通用交互接口,保持最简化原则。
+
+**与现有方案的关系**:
+- [A2A 跨设备通信](./a2a-cross-device.md):内部 Agent 间通信(基于 Trace API)
+- **MAMP 协议**(本文档):与外部 Agent 系统的通用交互
+
+---
+
+## 核心设计原则
+
+1. **最小化协议**:只定义消息信封,不管内容格式
+2. **适配器模式**:通过适配器层与内部系统集成
+3. **松耦合**:各家 Agent 保持独立实现
+4. **渐进式**:先实现基础功能,需要时再扩展
+
+---
+
+## 消息格式
+
+### 基础消息结构
+
+```json
+{
+  "protocol": "mamp/1.0",
+  "message_id": "msg-uuid-123",
+  "conversation_id": "conv-uuid-456",
+  "from": "agent://your-domain.com/agent-123",
+  "to": "agent://other-domain.com/agent-456",
+  "content": [...],
+  "metadata": {
+    "timestamp": "2026-03-04T10:00:00Z"
+  }
+}
+```
+
+### 字段说明
+
+| 字段 | 类型 | 必需 | 说明 |
+|------|------|------|------|
+| `protocol` | string | 是 | 协议版本标识 |
+| `message_id` | string | 是 | 消息唯一标识 |
+| `conversation_id` | string | 否 | 对话标识(不提供则新建对话) |
+| `from` | string | 是 | 发送方 Agent URI |
+| `to` | string | 是 | 接收方 Agent URI |
+| `content` | string/array | 是 | 消息内容(支持多模态) |
+| `metadata` | object | 是 | 元数据(时间戳等) |
+
+### 新建 vs 续跑
+
+**规则**:通过 `conversation_id` 字段判断
+
+- **无 `conversation_id`**(null 或不存在)→ 新建对话,接收方生成并返回 conversation_id
+- **有 `conversation_id`** → 续跑对话,接收方查找对应的内部 trace_id
+
+**conversation_id 与 trace_id 的关系**:
+- `conversation_id`:跨 Agent 的对话标识符,双方共享
+- `trace_id`:每个 Agent 内部的执行记录,各自独立
+- 每个 Agent 维护 `conversation_id → trace_id` 映射
+
+---
+
+## 多模态内容格式
+
+### Content 结构
+
+参考 Anthropic SDK 和现有多模态实现(`agent/docs/multimodal.md`):
+
+```json
+{
+  "content": [
+    {
+      "type": "text",
+      "text": "这是文本内容"
+    },
+    {
+      "type": "image",
+      "source": {
+        "type": "url",
+        "url": "https://...",
+        "media_type": "image/png"
+      }
+    },
+    {
+      "type": "image",
+      "source": {
+        "type": "base64",
+        "media_type": "image/jpeg",
+        "data": "base64..."
+      }
+    },
+    {
+      "type": "code",
+      "language": "python",
+      "code": "def hello(): pass"
+    },
+    {
+      "type": "file",
+      "name": "report.pdf",
+      "mime_type": "application/pdf",
+      "source": {
+        "type": "url",
+        "url": "https://..."
+      }
+    }
+  ]
+}
+```
+
+### 纯文本简写
+
+```json
+{
+  "content": "纯文本消息"
+}
+```
+
+等价于:
+
+```json
+{
+  "content": [{"type": "text", "text": "纯文本消息"}]
+}
+```
+
+---
+
+## Agent Card(身份与能力)
+
+每个 Agent 提供静态的 card 端点,用于身份识别和能力发现。
+
+### 端点
+
+```
+GET https://your-agent.com/mamp/v1/card
+```
+
+### 响应格式
+
+```json
+{
+  "protocol": "mamp/1.0",
+  "agent_id": "agent://your-domain.com/agent-123",
+  "name": "Code Analyst",
+  "description": "专注于代码分析的 Agent",
+
+  "owner": {
+    "user_id": "user-789",
+    "user_name": "张三",
+    "organization": "YourCompany"
+  },
+
+  "device": {
+    "device_id": "device-mac-001",
+    "device_name": "MacBook Pro",
+    "location": "Beijing Office",
+    "platform": "darwin"
+  },
+
+  "capabilities": {
+    "content_types": ["text", "image", "code"],
+    "max_message_size": 10485760,
+    "streaming": true,
+    "async": true,
+    "tools": ["code_analysis", "file_read", "web_search"]
+  },
+
+  "access": {
+    "public": false,
+    "allowed_agents": ["agent://trusted.com/*"],
+    "require_auth": true
+  }
+}
+```
+
+---
+
+## 传输层
+
+### HTTP REST(最简实现)
+
+**发送消息**:
+
+```http
+POST https://other-agent.com/mamp/v1/messages
+Content-Type: application/json
+Authorization: Bearer {api_key}
+
+{MAMP 消息体}
+```
+
+**响应**:
+
+```json
+{
+  "conversation_id": "conv-abc-123",
+  "message_id": "msg-xyz-456",
+  "status": "received"
+}
+```
+
+**错误响应**:
+
+```json
+{
+  "error": "conversation_not_found",
+  "message": "Conversation conv-xxx not found",
+  "status_code": 404
+}
+```
+
+### 可选扩展
+
+- **WebSocket**:实时双向通信
+- **Server-Sent Events**:流式响应
+- **Message Queue**:异步消息(NATS/Redis)
+
+---
+
+## 寻址方案
+
+使用 URI 格式:`agent://domain/agent-id`
+
+**示例**:
+- `agent://your-domain.com/trace-123` - 你的 Agent
+- `agent://claude.ai/session-456` - Claude
+- `agent://openai.com/assistant-789` - OpenAI Assistant
+
+每个 Agent 系统自己决定如何解析 `agent-id` 部分。
+
+---
+
+## 系统集成
+
+### 三层架构
+
+```
+┌─────────────────────────────────────────────────┐
+│ Layer 3: 内部 Agent 逻辑                         │
+│ (Trace, Goal, Messages...)                      │
+└─────────────────────────────────────────────────┘
+                    ↕
+┌─────────────────────────────────────────────────┐
+│ Layer 2: MAMP 适配器                             │
+│ - 内部格式 ↔ MAMP 格式转换                        │
+│ - conversation_id ↔ trace_id 映射                │
+└─────────────────────────────────────────────────┘
+                    ↕
+┌─────────────────────────────────────────────────┐
+│ Layer 1: 传输层(HTTP/WebSocket/MQ)             │
+└─────────────────────────────────────────────────┘
+```
+
+### 接收端实现
+
+**实现位置**:`agent/trace/mamp_api.py`
+
+```python
+@app.post("/mamp/v1/messages")
+async def receive_mamp_message(msg: dict):
+    """接收外部 Agent 的 MAMP 消息"""
+
+    conv_id = msg.get("conversation_id")
+
+    if not conv_id:
+        # 新建对话
+        conv_id = f"conv-{generate_uuid()}"
+
+        # 创建新 Trace
+        async for item in runner.run(
+            messages=[{"role": "user", "content": msg["content"]}],
+            config=RunConfig(
+                context={
+                    "mamp_conversation_id": conv_id,
+                    "mamp_from": msg["from"]
+                }
+            )
+        ):
+            if isinstance(item, Trace):
+                await store_conversation_mapping(conv_id, item.trace_id)
+
+        return {
+            "conversation_id": conv_id,
+            "message_id": msg["message_id"],
+            "status": "received"
+        }
+
+    else:
+        # 续跑对话
+        trace_id = await get_trace_by_conversation_id(conv_id)
+        if not trace_id:
+            raise HTTPException(404, f"Conversation {conv_id} not found")
+
+        await runner.run(
+            messages=[{"role": "user", "content": msg["content"]}],
+            config=RunConfig(trace_id=trace_id)
+        )
+
+        return {
+            "conversation_id": conv_id,
+            "message_id": msg["message_id"],
+            "status": "received"
+        }
+```
+
+### 发送端实现
+
+**实现位置**:`agent/tools/builtin/mamp_adapter.py`
+
+```python
+@tool(description="与外部 Agent 通信")
+async def send_to_agent(
+    target_agent: str,
+    message: str,
+    conversation_id: Optional[str] = None,
+    ctx: ToolContext = None
+) -> ToolResult:
+    """
+    发送消息到外部 Agent
+
+    Args:
+        target_agent: 目标 Agent URI (agent://domain/id)
+        message: 消息内容
+        conversation_id: 对话 ID(可选,不提供则新建)
+    """
+
+    # 构建 MAMP 消息
+    mamp_msg = {
+        "protocol": "mamp/1.0",
+        "message_id": generate_uuid(),
+        "from": f"agent://{config.domain}/{ctx.trace_id}",
+        "to": target_agent,
+        "content": message,
+        "metadata": {"timestamp": datetime.now().isoformat()}
+    }
+
+    if conversation_id:
+        mamp_msg["conversation_id"] = conversation_id
+
+    # 发送
+    agent_url = parse_agent_url(target_agent)
+    response = await http_post(f"{agent_url}/mamp/v1/messages", mamp_msg)
+
+    # 新建时存储映射
+    if not conversation_id:
+        await store_conversation_mapping(
+            response["conversation_id"],
+            ctx.trace_id
+        )
+
+    return ToolResult(
+        title=f"已发送到 {target_agent}",
+        output=f"Conversation ID: {response['conversation_id']}",
+        long_term_memory=f"与 {target_agent} 的对话 {response['conversation_id']}"
+    )
+```
+
+### conversation_id 映射存储
+
+**实现位置**:`agent/trace/conversation_store.py`
+
+```python
+class ConversationStore:
+    """管理 MAMP conversation_id 和内部 trace_id 的映射"""
+
+    def __init__(self, base_dir: str = ".trace"):
+        self.mapping_file = Path(base_dir) / "mamp_conversations.json"
+
+    async def store_mapping(self, conversation_id: str, trace_id: str):
+        """存储映射关系"""
+        mappings = await self._load_mappings()
+        mappings[conversation_id] = {
+            "trace_id": trace_id,
+            "created_at": datetime.now().isoformat(),
+            "last_message_at": datetime.now().isoformat()
+        }
+        await self._save_mappings(mappings)
+
+    async def get_trace_id(self, conversation_id: str) -> Optional[str]:
+        """根据 conversation_id 查找 trace_id"""
+        mappings = await self._load_mappings()
+        mapping = mappings.get(conversation_id)
+        return mapping["trace_id"] if mapping else None
+```
+
+---
+
+## 使用示例
+
+### 新建对话
+
+```python
+# 调用外部 Agent
+result = await send_to_agent(
+    target_agent="agent://other.com/code-analyst",
+    message="帮我分析这段代码的性能"
+)
+# 返回: {"conversation_id": "conv-abc-123", ...}
+```
+
+### 续跑对话
+
+```python
+# 继续之前的对话
+result = await send_to_agent(
+    target_agent="agent://other.com/code-analyst",
+    message="那如果用异步方案呢?",
+    conversation_id="conv-abc-123"
+)
+```
+
+### 多模态消息
+
+```python
+# 发送图片
+result = await send_to_agent(
+    target_agent="agent://other.com/image-analyst",
+    message={
+        "content": [
+            {"type": "text", "text": "分析这张图片"},
+            {
+                "type": "image",
+                "source": {
+                    "type": "base64",
+                    "media_type": "image/png",
+                    "data": encode_image_base64("screenshot.png")
+                }
+            }
+        ]
+    }
+)
+```
+
+---
+
+## 与现有标准的关系
+
+MAMP 可以作为其他标准的"翻译层":
+
+- **MCP (Model Context Protocol)** → 写 MCP ↔ MAMP 适配器
+- **OpenAI Assistant API** → 写 OpenAI ↔ MAMP 适配器
+- **自定义协议** → 写对应的适配器
+
+**核心思想**:不要试图统一所有 Agent 的内部实现,而是提供一个最薄的互操作层。
+
+---
+
+## 可选扩展
+
+如果需要更丰富的功能,可以逐步添加:
+
+- **认证**:在 metadata 中加 `auth_token`
+- **流式传输**:使用 Server-Sent Events 或 WebSocket
+- **异步回调**:加 `callback_url` 字段
+- **能力协商**:通过 `/mamp/v1/card` 端点
+- **错误处理**:标准化错误码
+
+---
+
+## 实现路线图
+
+### Phase 1:基础协议(1-2 周)
+
+**目标**:实现最简 MAMP 协议
+
+**任务**:
+1. 实现 `/mamp/v1/messages` 端点(接收消息)
+2. 实现 `/mamp/v1/card` 端点(Agent 身份)
+3. 实现 `send_to_agent` 工具(发送消息)
+4. 实现 `ConversationStore`(映射管理)
+5. 支持纯文本消息
+
+### Phase 2:多模态支持(1 周)
+
+**目标**:支持图片、代码等多模态内容
+
+**任务**:
+1. 扩展 content 格式处理
+2. 集成现有多模态实现(`agent/llm/prompts/wrapper.py`)
+3. 支持 base64 和 URL 两种图片传输方式
+
+### Phase 3:增强功能(可选)
+
+**目标**:认证、流式、异步等高级功能
+
+**任务**:
+1. API Key 认证
+2. WebSocket 流式传输
+3. 异步回调机制
+4. 错误处理和重试
+
+---
+
+## 相关文档
+
+- [A2A 跨设备通信](./a2a-cross-device.md):内部 Agent 间通信方案
+- [多模态支持](../../agent/docs/multimodal.md):图片、PDF 处理
+- [工具系统](../../agent/docs/tools.md):工具定义、注册
+- [Agent 框架](../README.md):核心 Agent 能力

+ 114 - 0
docs/research/a2a-protocols.md

@@ -0,0 +1,114 @@
+# Agent2Agent (A2A) 通信协议调研
+
+**调研日期:** 2026-03-03
+
+## 一、行业标准协议
+
+### 1. Google A2A Protocol (2025.04)
+- **定位:** Agent间任务协调和协作
+- **特性:** 标准化消息格式、能力协商、异步通信
+- **适用:** 企业级跨平台Agent协作
+
+### 2. Anthropic MCP (2024.11)
+- **定位:** AI助手与工具/数据系统连接
+- **特性:** JSON-RPC 2.0、即插即用工具
+- **适用:** Agent与工具交互(非Agent间通信)
+- **采用:** OpenAI (2025.03)、Google DeepMind
+
+### 3. IBM ACP (2025初)
+- **定位:** 基于HTTP的Agent消息传递
+- **特性:** 消息代理(Kafka/RabbitMQ)、会话跟踪
+- **适用:** 生产级系统的模块化和可追溯性
+
+### 4. Huawei A2A-T (2026.03开源)
+- **定位:** A2A协议的扩展实现
+- **状态:** 刚开源,推动标准应用
+
+## 二、主流框架实现
+
+### AutoGen (Microsoft)
+- **通信模式:** 对话式多Agent协作
+- **核心:** ConversableAgent + GroupChat
+- **消息管理:** 每个Agent维护对话历史,GroupChat维护全局记录
+- **特点:** 自然语言驱动、支持人机协作
+
+### LangGraph (LangChain)
+- **通信模式:** 基于状态图的消息传递
+- **核心:** State Graph + Persistent State + Message Bus
+- **消息管理:** 状态图管理 + 检查点机制
+- **特点:** 生产级、可追溯、原生支持A2A协议
+
+### CrewAI
+- **通信模式:** 基于角色的任务委派
+- **核心:** Role-Based Agents + Task Delegation + Crew Coordination
+- **消息管理:** Crew级任务历史 + 委派记录
+- **特点:** 类似人类团队、层次化任务分配
+
+## 三、通信模式对比
+
+| 模式 | 优点 | 缺点 | 适用场景 |
+|------|------|------|----------|
+| **直接调用** | 简单、低延迟 | 紧耦合、难扩展 | 小规模简单协作 |
+| **消息队列** | 解耦、异步、可靠 | 复杂、需基础设施 | 企业级大规模系统 |
+| **共享状态** | 知识全局可见、紧密协调 | 并发控制、状态冲突 | 高度协同团队 |
+| **混合模式** | 灵活、可优化 | 架构复杂 | 复杂生产系统 |
+
+## 四、消息历史管理策略
+
+1. **滑动窗口:** 保留最近N条消息
+2. **智能截断:** 基于重要性评分删除
+3. **自动总结:** 接近限制时总结历史(Claude Code使用)
+4. **分层存储:** 短期完整 + 长期总结
+5. **溢出修剪:** 从最旧消息开始修剪
+
+## 五、关键挑战
+
+### 1. 消息历史维护
+- 上下文窗口限制
+- 需要智能压缩策略
+- 跨Agent的上下文共享
+
+### 2. 异步通信
+- 事件驱动架构
+- 回调机制
+- 状态更新和轮询
+
+### 3. 多Agent协作复杂性
+- 协调模式(集中式 vs 去中心化)
+- 冲突解决
+- 死锁预防
+- 可观测性
+
+## 六、标准化趋势
+
+**当前状态(2024-2026):**
+- 协议层分化:MCP(工具层)、A2A(协作层)、ACP(传输层)
+- 行业共识形成中:Google、OpenAI、Anthropic、IBM、Huawei推动
+- 互操作性是关键
+
+**未来展望:**
+- 2026-2027:协议标准逐步成熟
+- 2028-2030:可能出现统一标准
+- 长期:Agent网络成为基础设施
+
+## 七、实践建议
+
+### 架构设计
+1. **分层设计:** 工具层(MCP)+ 协作层(A2A)+ 传输层(ACP)
+2. **消息管理:** 自动总结 + 分层存储
+3. **异步处理:** 事件驱动 + 超时重试
+4. **可观测性:** 结构化日志 + 分布式追踪
+
+### 选择建议
+- **小规模:** AutoGen、CrewAI
+- **大规模:** LangGraph + ACP/A2A
+- **工具集成:** 优先MCP
+- **Agent协作:** 优先A2A
+
+## 参考资料
+
+- [Google A2A Protocol](https://a2a-protocol.org/latest/specification/)
+- [Anthropic MCP](https://modelcontextprotocol.io/specification/2025-06-18)
+- [Huawei A2A-T](https://www.huawei.com/en/news/2026/2/mwc-a2at-opensource)
+- [Agent Interoperability Survey](https://arxiv.org/html/2505.02279v1)
+- [Framework Comparison 2026](https://markaicode.com/crewai-vs-autogen-vs-langgraph-2026/)

+ 484 - 0
docs/research/a2a-trace-storage.md

@@ -0,0 +1,484 @@
+# A2A跨设备Trace存储方案
+
+**问题:** 现有Trace存储在本地文件系统,跨设备时如何访问?
+
+## 场景分析
+
+### 场景1:云端Agent调用终端Agent
+
+```
+云端Agent (云端存储)
+    ↓ 调用
+终端Agent (终端存储)
+    ↓ 返回 sub_trace_id
+云端Agent 想 continue_from sub_trace_id
+    ↓ 问题:sub_trace在终端,云端访问不到!
+```
+
+### 场景2:终端Agent调用云端Agent
+
+```
+终端Agent (终端存储)
+    ↓ 调用
+云端Agent (云端存储)
+    ↓ 返回 sub_trace_id
+终端Agent 想 continue_from sub_trace_id
+    ↓ 问题:sub_trace在云端,终端访问不到!
+```
+
+## 方案对比
+
+### 方案1:远程Trace访问(推荐)
+
+#### 核心思想
+
+**Trace ID包含位置信息,通过HTTP API访问远程Trace**
+
+#### Trace ID格式
+
+```
+本地Trace: abc-123
+远程Trace: agent://terminal-agent-456/abc-123
+          ^^^^^^^^ ^^^^^^^^^^^^^^^^^ ^^^^^^^
+          协议      Agent地址          本地ID
+```
+
+#### 实现
+
+```python
+# agent/trace/remote_store.py
+
+class RemoteTraceStore:
+    """远程Trace存储代理"""
+
+    def __init__(self, agent_url: str, api_key: str):
+        self.agent_url = agent_url
+        self.api_key = api_key
+
+    async def get_trace(self, trace_id: str) -> Optional[Trace]:
+        """通过HTTP API获取远程Trace"""
+        response = await self._get(f"/api/traces/{trace_id}")
+        if response:
+            return Trace(**response)
+        return None
+
+    async def get_main_path_messages(
+        self, trace_id: str, head_sequence: int
+    ) -> List[Message]:
+        """获取远程Trace的消息"""
+        response = await self._get(
+            f"/api/traces/{trace_id}/messages",
+            params={"mode": "main_path", "head_sequence": head_sequence}
+        )
+        return [Message(**m) for m in response["messages"]]
+
+    async def add_message(self, message: Message) -> str:
+        """向远程Trace追加消息(续跑)"""
+        response = await self._post(
+            f"/api/traces/{message.trace_id}/messages",
+            data=message.to_dict()
+        )
+        return response["message_id"]
+
+
+# agent/trace/hybrid_store.py
+
+class HybridTraceStore:
+    """混合存储:本地 + 远程"""
+
+    def __init__(self, local_store: FileSystemTraceStore):
+        self.local_store = local_store
+        self.remote_stores = {}  # agent_url -> RemoteTraceStore
+
+    def _parse_trace_id(self, trace_id: str) -> tuple[str, str]:
+        """
+        解析Trace ID
+
+        返回: (location, local_id)
+        - location: "local" 或 agent_url
+        - local_id: 本地Trace ID
+        """
+        if trace_id.startswith("agent://"):
+            # agent://terminal-agent-456/abc-123
+            parts = trace_id[8:].split("/", 1)
+            return parts[0], parts[1]
+        else:
+            return "local", trace_id
+
+    async def get_trace(self, trace_id: str) -> Optional[Trace]:
+        """获取Trace(自动路由到本地或远程)"""
+        location, local_id = self._parse_trace_id(trace_id)
+
+        if location == "local":
+            return await self.local_store.get_trace(local_id)
+        else:
+            # 远程Trace
+            if location not in self.remote_stores:
+                # 创建远程存储代理
+                self.remote_stores[location] = RemoteTraceStore(
+                    agent_url=f"https://{location}",
+                    api_key=self._get_api_key(location)
+                )
+            return await self.remote_stores[location].get_trace(local_id)
+
+    async def add_message(self, message: Message) -> str:
+        """添加消息(自动路由)"""
+        location, local_id = self._parse_trace_id(message.trace_id)
+
+        if location == "local":
+            return await self.local_store.add_message(message)
+        else:
+            # 向远程Trace追加消息
+            return await self.remote_stores[location].add_message(message)
+```
+
+#### agent工具修改
+
+```python
+@tool(description="创建 Agent 执行任务")
+async def agent(
+    task: Union[str, List[str]],
+    continue_from: Optional[str] = None,  # 支持远程Trace ID
+    agent_url: Optional[str] = None,      # 新增:远程Agent地址
+    context: Optional[dict] = None,
+) -> Dict[str, Any]:
+    """
+    创建 Agent 执行任务
+
+    Args:
+        task: 任务描述
+        continue_from: 继续已有trace(支持远程Trace ID)
+        agent_url: 远程Agent地址(如果调用远程Agent)
+        context: 框架注入的上下文
+    """
+    store = context.get("store")  # HybridTraceStore
+
+    if agent_url:
+        # 调用远程Agent
+        result = await _call_remote_agent(agent_url, task, continue_from)
+        # 返回远程Trace ID
+        remote_trace_id = f"agent://{agent_url}/{result['sub_trace_id']}"
+        return {
+            **result,
+            "sub_trace_id": remote_trace_id,
+            "remote": True
+        }
+    else:
+        # 本地执行(现有逻辑)
+        if continue_from:
+            # 可能是远程Trace,HybridStore会自动处理
+            existing = await store.get_trace(continue_from)
+            if not existing:
+                return {"status": "failed", "error": "Trace not found"}
+
+        # ... 现有逻辑
+```
+
+#### 使用示例
+
+```python
+# 云端Agent调用终端Agent
+result1 = await agent(
+    task="分析本地项目",
+    agent_url="terminal-agent-456.local"
+)
+# 返回: {"sub_trace_id": "agent://terminal-agent-456.local/abc-123"}
+
+# 继续对话(自动访问远程Trace)
+result2 = await agent(
+    task="重点分析core模块",
+    continue_from=result1["sub_trace_id"],  # 远程Trace ID
+    agent_url="terminal-agent-456.local"
+)
+# HybridStore自动通过HTTP API访问远程Trace
+```
+
+#### 优势
+
+1. **透明** - Agent不需要关心Trace在哪里
+2. **灵活** - 支持本地和远程Trace
+3. **简单** - 只需要在Trace ID中编码位置信息
+4. **兼容** - 现有本地Trace不受影响
+
+#### 劣势
+
+1. **网络延迟** - 访问远程Trace需要HTTP请求
+2. **依赖网络** - 远程Agent必须在线
+
+---
+
+### 方案2:中心化Trace存储
+
+#### 核心思想
+
+**所有Agent共享同一个Trace存储(数据库)**
+
+#### 架构
+
+```
+云端Agent ──┐
+            ├──> 中心化Trace存储(PostgreSQL/MongoDB)
+终端Agent ──┘
+```
+
+#### 实现
+
+```python
+# agent/trace/db_store.py
+
+class DatabaseTraceStore:
+    """数据库Trace存储"""
+
+    def __init__(self, db_url: str):
+        self.db = connect(db_url)
+
+    async def create_trace(self, trace: Trace) -> str:
+        await self.db.traces.insert_one(trace.to_dict())
+        return trace.trace_id
+
+    async def get_trace(self, trace_id: str) -> Optional[Trace]:
+        doc = await self.db.traces.find_one({"trace_id": trace_id})
+        if doc:
+            return Trace(**doc)
+        return None
+
+    # ... 其他方法
+```
+
+#### 配置
+
+```yaml
+# config/storage.yaml
+trace_store:
+  type: database
+  url: postgresql://user:pass@db.cloud/traces
+  # 或
+  url: mongodb://db.cloud/traces
+```
+
+#### 优势
+
+1. **简单** - 所有Agent访问同一个存储,无需特殊处理
+2. **一致性** - 数据强一致性
+3. **查询能力** - 可以跨Trace查询和分析
+
+#### 劣势
+
+1. **依赖中心** - 需要中心化数据库
+2. **网络依赖** - 终端Agent必须能访问数据库
+3. **隐私问题** - 终端数据存储在云端
+4. **迁移成本** - 需要从文件系统迁移到数据库
+
+---
+
+### 方案3:Trace同步/缓存
+
+#### 核心思想
+
+**按需同步远程Trace到本地**
+
+#### 实现
+
+```python
+class CachedRemoteStore:
+    """带缓存的远程存储"""
+
+    def __init__(self, local_store, remote_url):
+        self.local_store = local_store
+        self.remote_url = remote_url
+        self.cache = {}  # trace_id -> Trace
+
+    async def get_trace(self, trace_id: str) -> Optional[Trace]:
+        # 1. 检查本地
+        trace = await self.local_store.get_trace(trace_id)
+        if trace:
+            return trace
+
+        # 2. 检查缓存
+        if trace_id in self.cache:
+            return self.cache[trace_id]
+
+        # 3. 从远程获取并缓存
+        trace = await self._fetch_remote(trace_id)
+        if trace:
+            self.cache[trace_id] = trace
+            # 可选:持久化到本地
+            await self.local_store.create_trace(trace)
+        return trace
+
+    async def add_message(self, message: Message) -> str:
+        # 同时写入本地和远程
+        local_id = await self.local_store.add_message(message)
+        await self._sync_to_remote(message)
+        return local_id
+```
+
+#### 优势
+
+1. **性能** - 本地缓存减少网络请求
+2. **离线能力** - 缓存后可以离线访问
+
+#### 劣势
+
+1. **一致性** - 缓存可能过期
+2. **复杂度** - 需要处理同步和冲突
+3. **存储开销** - 需要额外的本地存储
+
+---
+
+### 方案4:混合模式(推荐)
+
+#### 核心思想
+
+**结合方案1和方案2的优点**
+
+- **组织内部**:使用中心化存储(数据库)
+- **跨组织**:使用远程访问(HTTP API)
+
+#### 架构
+
+```
+组织内部:
+云端Agent ──┐
+            ├──> 组织数据库
+职能Agent ──┘
+
+跨组织:
+组织A Agent <──HTTP API──> 组织B Agent
+```
+
+#### 配置
+
+```yaml
+# config/storage.yaml
+trace_store:
+  # 组织内部使用数据库
+  internal:
+    type: database
+    url: postgresql://org-db/traces
+
+  # 跨组织使用远程访问
+  external:
+    type: remote
+    agents:
+      - url: https://partner-org.com
+        api_key: xxx
+```
+
+#### 优势
+
+1. **灵活** - 根据场景选择最优方案
+2. **性能** - 组织内部低延迟
+3. **隐私** - 跨组织数据不共享存储
+
+---
+
+## 推荐方案
+
+### MVP阶段(Phase 1-2):方案1 - 远程Trace访问
+
+**理由:**
+1. **最小改动** - 只需要添加RemoteTraceStore和HybridStore
+2. **灵活** - 支持任意拓扑
+3. **隐私友好** - 数据不离开设备
+4. **渐进式** - 可以逐步迁移到方案4
+
+**实现步骤:**
+1. 实现RemoteTraceStore(通过HTTP API访问)
+2. 实现HybridTraceStore(路由到本地或远程)
+3. 修改agent工具支持agent_url参数
+4. 在Trace ID中编码位置信息
+
+### 长期(Phase 3+):方案4 - 混合模式
+
+**理由:**
+1. **组织内部高效** - 数据库存储,低延迟
+2. **跨组织灵活** - HTTP API,保护隐私
+3. **可扩展** - 支持复杂场景
+
+## 实现细节
+
+### 1. Trace ID格式
+
+```python
+# 本地Trace
+"abc-123"
+
+# 远程Trace(完整格式)
+"agent://terminal-agent-456.local:8000/abc-123"
+
+# 远程Trace(简化格式,使用注册的agent_id)
+"@terminal-agent-456/abc-123"
+```
+
+### 2. Agent注册表
+
+```yaml
+# config/agents.yaml
+agents:
+  terminal-agent-456:
+    url: https://terminal-agent-456.local:8000
+    api_key: ak_xxx
+    type: terminal
+```
+
+### 3. API端点
+
+```python
+# 必需的API端点(用于远程访问)
+GET  /api/traces/{trace_id}                    # 获取Trace
+GET  /api/traces/{trace_id}/messages           # 获取消息
+POST /api/traces/{trace_id}/run                # 续跑
+POST /api/traces/{trace_id}/messages           # 追加消息
+```
+
+### 4. 认证和授权
+
+```python
+# 每个Agent有自己的API Key
+headers = {
+    "Authorization": f"Bearer {api_key}"
+}
+
+# 权限检查
+if trace.uid != request.user_id:
+    raise Forbidden("Cannot access other user's trace")
+```
+
+### 5. 性能优化
+
+```python
+# 批量获取消息
+GET /api/traces/{trace_id}/messages?limit=100&offset=0
+
+# 增量同步
+GET /api/traces/{trace_id}/messages?since_sequence=50
+
+# 压缩传输
+headers = {"Accept-Encoding": "gzip"}
+```
+
+## 总结
+
+**推荐路线:**
+
+1. **Phase 1(MVP)** - 实现方案1(远程Trace访问)
+   - 最小改动
+   - 快速验证跨设备A2A
+   - 2-3周
+
+2. **Phase 2** - 优化和增强
+   - 添加缓存
+   - 批量API
+   - 性能优化
+   - 2-3周
+
+3. **Phase 3(可选)** - 迁移到方案4(混合模式)
+   - 组织内部使用数据库
+   - 跨组织使用远程访问
+   - 4-6周
+
+**关键优势:**
+- 渐进式实现
+- 最小化风险
+- 保持灵活性

+ 0 - 0
examples/analyze_story/README.md → examples/archive/analyze_story/README.md


+ 0 - 0
examples/analyze_story/analysis_results.json → examples/archive/analyze_story/analysis_results.json


+ 0 - 0
examples/analyze_story/analyze_samples.py → examples/archive/analyze_story/analyze_samples.py


+ 0 - 0
examples/analyze_story/generate_report.py → examples/archive/analyze_story/generate_report.py


+ 0 - 0
examples/analyze_story/input/中国合伙人.pdf → examples/archive/analyze_story/input/中国合伙人.pdf


+ 0 - 0
examples/analyze_story/input/大奉打更人.txt → examples/archive/analyze_story/input/大奉打更人.txt


+ 0 - 0
examples/analyze_story/input/搜神记.txt → examples/archive/analyze_story/input/搜神记.txt


+ 0 - 0
examples/analyze_story/input/无双.docx → examples/archive/analyze_story/input/无双.docx


+ 0 - 0
examples/analyze_story/input/雪中悍刀行.txt → examples/archive/analyze_story/input/雪中悍刀行.txt


+ 0 - 0
examples/analyze_story/input/魔道祖师.txt → examples/archive/analyze_story/input/魔道祖师.txt


+ 0 - 0
examples/analyze_story/knowledge/01_Scene_Sequel_Structure.md → examples/archive/analyze_story/knowledge/01_Scene_Sequel_Structure.md


+ 0 - 0
examples/analyze_story/knowledge/01_save_the_cat_beat_sheet.md → examples/archive/analyze_story/knowledge/01_save_the_cat_beat_sheet.md


Неке датотеке нису приказане због велике количине промена