diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index b30a42b2..00000000 --- a/docs/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# ReMe 仓库文档 - -本目录保存 ReMe 仓库 README 直接引用的中英文补充说明和图片资源,不作为文档站点的构建或部署来源。 - -面向用户发布的中英文文档位于 [agentscope-ai/docs](https://github.com/agentscope-ai/docs) 仓库,并由该仓库统一完成版本管理和 Mintlify 部署。 - -## 目录用途 - -```text -docs/ -├── README.md 本目录的维护说明 -├── doc.md 当前文档设计与维护边界 -├── en/ README 引用的英文补充说明 -├── zh/ README 引用的中文补充说明 -└── figure/ ReMe README 使用的图片资源 -``` - -## 维护原则 - -- `en/` 和 `zh/` 保持精简,服务 README 中需要进一步解释的功能与场景;修改路径时同步更新 README 链接。 -- 具体实现以源码、schema、测试和运行时帮助为准,避免维护重复且容易过期的开发手册。 -- README 引用的图片保留在 `figure/`;发布文档需要图片时,在统一文档仓库的 `images/reme/` 中维护对应副本。 -- 网页文档、导航、版本和部署在统一文档仓库中维护。 diff --git a/docs/doc.md b/docs/doc.md deleted file mode 100644 index b345ec88..00000000 --- a/docs/doc.md +++ /dev/null @@ -1,85 +0,0 @@ -# ReMe 文档设计 - -本文定义 ReMe 文档的内容边界和维护方式。目标是让文档保持精简、稳定,并适合用户与 AI coding agent 快速理解。 - -## 两类文档,两种职责 - -| 位置 | 用途 | 是否部署 | -|---|---|---| -| `ReMe/docs/` | README 引用的中英文补充说明和图片 | 否 | -| `agentscope-ai/docs/reme//` | 面向用户的中英文产品文档 | 是 | - -ReMe 仓库维护 `docs/en/`、`docs/zh/` 中供 README 直接引用的页面,但不把它们作为网页部署来源。网站内容、发布、版本选择、 -导航和重定向都由统一文档仓库负责。 - -## 内容原则 - -### Concepts 只讲理念 - -Concepts 应解释 ReMe 为什么这样设计,而不是逐项描述组件和流水线实现。核心判断包括: - -- **Memory as File**:记忆首先是用户拥有、可读写和可迁移的文件。 -- **Memory from Experience**:长期记忆来自经验的提炼、修正和合并,而不是无限累积上下文。 -- **Human-Agent Shared Memory**:用户和 Agent 共同读写同一份可见记忆。 -- **Connected and Traceable**:长期结论可以通过链接回到来源和上下文。 - -算法、索引、Job、Step 和存储实现只有在帮助解释理念取舍时才进入 Concepts。 - -### Development 保持轻量 - -现代开发主要由 AI 直接阅读源码、schema 和测试完成。Development 只需要提供: - -- 开发环境和最小验证命令; -- 代码目录入口; -- 兼容性与贡献要求; -- 哪些源码或 schema 是权威依据。 - -不为每个类、组件或扩展点编写重复的开发手册,也不维护 `generic_agent` 一类泛化教程。 - -### Reference 只记录稳定契约 - -Reference 记录 workspace、配置入口、CLI、HTTP、MCP 和文件格式的稳定语义。精确参数交给运行时帮助、Pydantic schema 和源码,避免文档复制一份容易失真的接口定义。 - -### Guides 只保留已验证路径 - -接入文档应对应真实、可验证的工作流。目前优先维护 Claude Code、QwenPaw,以及 Skill、CLI、MCP、HTTP、Python 的选择说明。没有可验证实现的框架不提前创建占位页。 - -## 发布文档结构 - -ReMe 参考 AgentScope 的版本目录和导航方式: - -```text -agentscope-ai/docs/ -├── reme/ -│ └── 0.4.0.6/ -│ ├── en/ -│ └── zh/ -└── images/ - └── reme/ -``` - -每个语言版本保持三组导航: - -1. **Get Started / 快速开始**:Index、Overview、Quick Start、Concepts。 -2. **Integrate / 接入**:接入选择、Claude Code、QwenPaw。 -3. **Reference / 查阅与参与**:Reference、Support、Contributing。 - -ReMe 使用项目级别的 `/reme/latest/` 和 `/reme/stable/` 别名,不影响 AgentScope 自己的 `/latest/` 与 `/stable/`。 - -## 变更应该写在哪里 - -| 变更类型 | ReMe 仓库 | 统一文档仓库 | -|---|---|---| -| 产品理念或长期设计判断 | 更新 `docs/doc.md` 或相关设计记录 | 必要时同步 Concepts | -| 用户可见的安装、配置或行为 | 源码、schema、测试;影响 README 时同步 `docs/en/`、`docs/zh/` | 更新对应版本的用户文档 | -| 内部重构或组件调整 | 以代码和测试表达 | 稳定契约未变时无需更新 | -| README 图片 | 更新 `docs/figure/` | 发布页使用时同步到 `images/reme/` | -| 新版本发布 | 更新版本号和代码 | 新建版本目录、双语导航与 ReMe 别名 | - -## 质量要求 - -- 每个用户流程必须能够在当前版本运行和验证。 -- 文档不复制能够从代码可靠获得的细节。 -- 删除过期内容优先于继续叠加补丁说明。 -- 中英文页面保持信息等价,不要求逐句直译。 -- 发布前在统一文档仓库运行 Mintlify 严格校验。 diff --git a/docs/en/proactive.md b/docs/en/proactive.md index 764db886..7b13bd6d 100644 --- a/docs/en/proactive.md +++ b/docs/en/proactive.md @@ -37,7 +37,7 @@ Parameters: | Parameter | Purpose | |---|---| | `date` | Date to read in `YYYY-MM-DD` format. When empty, use today in the application's timezone. | -| `include_content` | Whether to return the raw YAML in metadata. Defaults to `true`. | +| `include_content` | Whether to return the raw YAML in the answer and metadata. Defaults to `true`. | ## Input Contract @@ -63,7 +63,9 @@ Only the `topics` list is parsed into structured results. Every topic requires a ## Return Value -When the file is read successfully, `proactive_step` writes these values to standard response metadata: +When the file is read successfully, `proactive_step` returns `summary` and `topics` in the primary answer. When +`include_content=true`, the answer also contains `content`. The same result fields remain available in standard response +metadata: | Field | Description | |---|---| @@ -75,12 +77,27 @@ When the file is read successfully, `proactive_step` writes these values to stan | `error` | Read or parse error. | | `summary` | Short summary. | -When the file exists and parses successfully, the answer looks like: +When the file exists and parses successfully, the answer is structured data. For example: -```text -Read 3 proactive topic(s) from daily/2026-06-20/interests.yaml +```json +{ + "summary": "Read 1 proactive topic(s) from daily/2026-06-20/interests.yaml", + "topics": [ + { + "title": "Quality regression in the memory retrieval pipeline", + "reason": "The user has recently made repeated changes to search, node_search, and dream integration.", + "evidence": "daily/2026-06-20/session.md", + "keywords": ["memory search", "auto dream"], + "paths": ["daily/2026-06-20/session.md"] + } + ], + "content": "date: 2026-06-20\n..." +} ``` +With `include_content=false`, the `content` field is omitted from the answer. Missing-file and read-error answers remain +explicit `Skipped: ...` and `Error: ...` messages, respectively. + A missing file is not an error. The call succeeds with a skipped result: ```text diff --git a/docs/zh/proactive.md b/docs/zh/proactive.md index adf2b23b..5880d218 100644 --- a/docs/zh/proactive.md +++ b/docs/zh/proactive.md @@ -34,7 +34,7 @@ proactive: | 参数 | 作用 | |-------------------|----------------------------------------| | `date` | 要读取的日期,格式为 `YYYY-MM-DD`。为空时使用应用时区中的今天。 | -| `include_content` | 是否在 metadata 中返回 YAML 原文,默认 `true`。 | +| `include_content` | 是否在 answer 和 metadata 中返回 YAML 原文,默认 `true`。 | ## 输入契约 @@ -59,7 +59,8 @@ topics: ## 返回结果 -成功读取时,`proactive_step` 会把结果写入标准 response metadata: +成功读取时,`proactive_step` 会在主要 answer 中返回 `summary` 和 `topics`;当 `include_content=true` 时还会返回 +`content`。相同的结果字段也会保留在标准 response metadata 中: | 字段 | 说明 | |-----------|----------------------------------------| @@ -71,12 +72,27 @@ topics: | `error` | 读取或解析异常。 | | `summary` | 简短摘要。 | -文件存在且解析成功时,answer 类似: +文件存在且解析成功时,answer 是结构化数据,例如: -```text -Read 3 proactive topic(s) from daily/2026-06-20/interests.yaml +```json +{ + "summary": "Read 1 proactive topic(s) from daily/2026-06-20/interests.yaml", + "topics": [ + { + "title": "记忆检索链路的质量回归", + "reason": "用户最近反复修改了 search、node_search 和 dream integration。", + "evidence": "daily/2026-06-20/session.md", + "keywords": ["memory search", "auto dream"], + "paths": ["daily/2026-06-20/session.md"] + } + ], + "content": "date: 2026-06-20\n..." +} ``` +当 `include_content=false` 时,answer 不包含 `content` 字段。文件缺失和读取失败仍分别返回明确的 +`Skipped: ...` 和 `Error: ...` 消息。 + 文件不存在时不会报错,而是成功返回 skipped: ```text diff --git a/reme/config/default.yaml b/reme/config/default.yaml index d42dd9f6..d541aa8e 100644 --- a/reme/config/default.yaml +++ b/reme/config/default.yaml @@ -191,7 +191,7 @@ jobs: default: "" include_content: type: boolean - description: "whether to include the raw YAML content in response metadata" + description: "whether to include the raw YAML content in the response answer and metadata" default: true steps: - backend: proactive_step diff --git a/reme/schema/response.py b/reme/schema/response.py index 89cbce33..a8d3a2f2 100644 --- a/reme/schema/response.py +++ b/reme/schema/response.py @@ -6,10 +6,14 @@ from pydantic import BaseModel, ConfigDict, Field class Response(BaseModel): - """Standard response envelope; extra fields allowed for endpoint-specific output.""" + """Standard response envelope; extra fields allowed for endpoint-specific output. + + ``answer`` is the primary tool result and must be sufficient for the next LLM action. + ``metadata`` is auxiliary request context for programmatic clients and diagnostics. + """ model_config = ConfigDict(extra="allow") - answer: str | Any = Field(default="", description="Response content or result data") + answer: str | Any = Field(default="", description="Primary response content or result data exposed to tool callers") success: bool = Field(default=True, description="Whether the operation succeeded") - metadata: dict = Field(default_factory=dict, description="Additional context and diagnostics") + metadata: dict = Field(default_factory=dict, description="Auxiliary request context and diagnostics") diff --git a/reme/steps/evolve/dream/proactive.py b/reme/steps/evolve/dream/proactive.py index 07da7897..7463970f 100644 --- a/reme/steps/evolve/dream/proactive.py +++ b/reme/steps/evolve/dream/proactive.py @@ -26,7 +26,7 @@ class ProactiveStep(BaseStep): if not abs_path.is_file(): result.skipped, result.summary = True, f"Skipped: interests file not found at {rel_path}" self.logger.info(f"[{self.name}] skip missing path={rel_path}") - return self._finish(True, result) + return self._finish(True, result, include_content=include_content) try: self.logger.info(f"[{self.name}] read start path={rel_path}") result.content = abs_path.read_text(encoding="utf-8") if include_content else "" @@ -37,15 +37,24 @@ class ProactiveStep(BaseStep): except Exception as e: # noqa: BLE001 result.error, result.summary = f"{type(e).__name__}: {e}", "" self.logger.error(f"[{self.name}] read failed path={rel_path}: {result.error}") - return self._finish(False, result) + return self._finish(False, result, include_content=include_content) result.summary = f"Read {len(result.topics)} proactive topic(s) from {rel_path}" - return self._finish(True, result) + return self._finish(True, result, include_content=include_content) - def _finish(self, success: bool, result: ProactiveResult): + def _finish(self, success: bool, result: ProactiveResult, *, include_content: bool): assert self.context is not None self.context.response.success = success - self.context.response.answer = result.summary if success else f"Error: {result.error}" + if not success: + self.context.response.answer = f"Error: {result.error}" + elif result.skipped: + self.context.response.answer = result.summary + else: + self.context.response.answer = { + "summary": result.summary, + "topics": result.topics, + **({"content": result.content} if include_content else {}), + } self.context.response.metadata.update(result.model_dump()) self.logger.info(f"[{self.name}] finish success={success} answer={self.context.response.answer!r}") return self.context.response diff --git a/skills/reme_memory/SKILL.md b/skills/reme_memory/SKILL.md index 62b2831f..b4a7133c 100644 --- a/skills/reme_memory/SKILL.md +++ b/skills/reme_memory/SKILL.md @@ -88,7 +88,8 @@ Use `proactive` to read interest topics generated by `auto_dream`: reme proactive date="" ``` -`proactive` only returns topics; the Agent decides whether and how to mention them to the user. +`proactive` returns structured topics and, by default, the source YAML content. Pass `include_content=false` when the raw +content is not needed. The Agent decides whether and how to mention the topics to the user. ## Integration Rules diff --git a/tests/unit/test_auto_dream.py b/tests/unit/test_auto_dream.py index 7560f096..b3c62bd7 100644 --- a/tests/unit/test_auto_dream.py +++ b/tests/unit/test_auto_dream.py @@ -3,15 +3,18 @@ import asyncio import tempfile from pathlib import Path +from unittest.mock import patch import yaml +from reme.components.application_context import ApplicationContext from reme.components.file_catalog import BaseFileCatalog from reme.components.file_store import BaseFileStore from reme.components.runtime_context import RuntimeContext from reme.schema import DreamState from reme.steps.evolve.dream.extract import DreamExtractStep from reme.steps.evolve.dream.finish import DreamFinishStep +from reme.steps.evolve.dream.proactive import ProactiveStep from reme.steps.evolve.dream.topics import DreamTopicsStep from reme.steps.evolve.dream.utils import parse_structured_reply, recent_dates, scan_day_files @@ -176,6 +179,88 @@ def test_topics_step_writes_only_target_date_interests(): asyncio.run(run()) +def test_proactive_answer_includes_topics_and_requested_content(tmp_path): + """Successful proactive reads expose useful data through the primary answer.""" + + async def run(): + content = ( + "date: 2026-05-28\n" + "topics:\n" + " - title: Retrieval quality\n" + " reason: Search behavior changed repeatedly.\n" + " evidence: daily/2026-05-28/session.md\n" + ) + _touch(tmp_path / "daily" / "2026-05-28" / "interests.yaml", content) + step = ProactiveStep(app_context=ApplicationContext(workspace_dir=str(tmp_path))) + + response = await step( + RuntimeContext(date="2026-05-28", include_content=True, file_store=_FileStore(tmp_path)), + ) + + assert response.success is True + assert response.answer == { + "summary": "Read 1 proactive topic(s) from daily/2026-05-28/interests.yaml", + "topics": [ + { + "title": "Retrieval quality", + "reason": "Search behavior changed repeatedly.", + "evidence": "daily/2026-05-28/session.md", + "keywords": [], + "paths": [], + }, + ], + "content": content, + } + assert response.metadata["topics"] == response.answer["topics"] + assert response.metadata["content"] == content + + asyncio.run(run()) + + +def test_proactive_answer_omits_unrequested_content(tmp_path): + """Raw YAML is absent from the primary answer when include_content is false.""" + + async def run(): + _touch( + tmp_path / "daily" / "2026-05-28" / "interests.yaml", + "topics:\n - title: Topic\n reason: Reason\n", + ) + step = ProactiveStep(app_context=ApplicationContext(workspace_dir=str(tmp_path))) + + response = await step( + RuntimeContext(date="2026-05-28", include_content=False, file_store=_FileStore(tmp_path)), + ) + + assert response.success is True + assert "content" not in response.answer + assert response.answer["topics"][0]["title"] == "Topic" + assert response.metadata["content"] == "" + + asyncio.run(run()) + + +def test_proactive_keeps_skipped_and_error_answers_explicit(tmp_path): + """Empty and failure outcomes remain distinguishable without reading metadata.""" + + async def run(): + step = ProactiveStep(app_context=ApplicationContext(workspace_dir=str(tmp_path))) + skipped = await step(RuntimeContext(date="2026-05-28", file_store=_FileStore(tmp_path))) + + assert skipped.success is True + assert skipped.answer == "Skipped: interests file not found at daily/2026-05-28/interests.yaml" + assert skipped.metadata["skipped"] is True + + _touch(tmp_path / "daily" / "2026-05-28" / "interests.yaml", "topics: []\n") + with patch("reme.steps.evolve.dream.proactive.load_yaml_topics", side_effect=ValueError("bad topics")): + failed = await step(RuntimeContext(date="2026-05-28", file_store=_FileStore(tmp_path))) + + assert failed.success is False + assert failed.answer == "Error: ValueError: bad topics" + assert failed.metadata["error"] == "ValueError: bad topics" + + asyncio.run(run()) + + def test_finish_does_not_checkpoint_failed_changed_paths(): """Finish does not checkpoint failed changed paths."""