docs(memory): document and harden hybrid gateway setup
This commit is contained in:
@ -27,3 +27,38 @@
|
||||
## 说明
|
||||
|
||||
后端已切到 Beaver 主线,不再保留旧实现、vendored 第三方 runtime 或迁移期旧命名兼容入口。所有 agent 运行都复用 `beaver.engine`,多 agent 协调通过 Beaver 自有 coordinator 和 `ExecutionGraph` 表达。
|
||||
|
||||
## Memory Gateway
|
||||
|
||||
Curated memory 始终启用:每轮仍会冻结并注入 `MEMORY.md` / `USER.md`,原有
|
||||
`memory` 工具也保持可用。`hybrid` 模式会额外启用独立的 Memory Gateway 层,
|
||||
每轮先调用 `/memories/search`,正常完成后调用一次 `/memories/add`,成功后再调用
|
||||
一次 `/memories/flush`。两套存储不会互相同步、覆盖或去重。
|
||||
|
||||
完整配置示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"memory": {
|
||||
"mode": "hybrid",
|
||||
"gateway": {
|
||||
"baseUrl": "http://127.0.0.1:8010",
|
||||
"userId": "gateway_test_user",
|
||||
"userKey": "uk_xxx",
|
||||
"appId": "default",
|
||||
"projectId": "default",
|
||||
"scope": ["current_chat", "resources"],
|
||||
"topK": 8,
|
||||
"timeoutSeconds": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `memory` 整段缺失时,默认采用隐式 `hybrid`;Gateway 凭证不完整会告警并只运行 curated memory。
|
||||
- 显式配置 `"mode": "hybrid"` 时,`baseUrl`、`userId` 和 `userKey` 缺失会导致启动失败。
|
||||
- 配置 `"mode": "curated"` 可关闭 Gateway,curated memory 行为不变。
|
||||
- `userKey` 是密钥,不应写入日志、状态响应或提交到版本库。
|
||||
- 容器访问宿主机 Gateway 时不能使用容器内的 `127.0.0.1`。应让 Gateway 监听
|
||||
`0.0.0.0`,并把 `baseUrl` 配成该 Docker 网络的宿主机网关地址。
|
||||
- 修改 memory 配置后需要重启 runtime,因为 Gateway 服务在 `EngineLoader` 启动时创建。
|
||||
|
||||
@ -209,13 +209,13 @@ class EngineLoader:
|
||||
"""装配当前主链需要的最小 runtime 对象。"""
|
||||
|
||||
workspace = self.workspace
|
||||
memory_gateway_service = self._resolve_memory_gateway_service()
|
||||
session_manager = self._session_manager or SessionManager(workspace)
|
||||
|
||||
curated_root = workspace / "memory" / "curated"
|
||||
curated_memory_store = self._curated_memory_store or MemoryStore(curated_root)
|
||||
memory_service = self._memory_service or MemoryService(curated_root, store=curated_memory_store)
|
||||
memory_service.initialize()
|
||||
memory_gateway_service = self._resolve_memory_gateway_service()
|
||||
run_memory_store = self._run_memory_store or RunMemoryStore(workspace / "memory" / "runs")
|
||||
skill_learning_store = self._skill_learning_store or SkillLearningStore(workspace / "memory" / "skills")
|
||||
|
||||
|
||||
@ -266,13 +266,18 @@ def _parse_memory(data: dict[str, Any]) -> MemoryConfig:
|
||||
parsed_timeout = _float(
|
||||
_first_config_value(gateway_raw.get("timeoutSeconds"), gateway_raw.get("timeout_seconds"))
|
||||
)
|
||||
scope = (
|
||||
_string_list(gateway_raw.get("scope"))
|
||||
if "scope" in gateway_raw
|
||||
else ["current_chat", "resources"]
|
||||
)
|
||||
gateway = MemoryGatewayConfig(
|
||||
base_url=_string(gateway_raw.get("baseUrl") or gateway_raw.get("base_url")) or "",
|
||||
user_id=_string(gateway_raw.get("userId") or gateway_raw.get("user_id")) or "",
|
||||
user_key=_string(gateway_raw.get("userKey") or gateway_raw.get("user_key")) or "",
|
||||
app_id=_string(gateway_raw.get("appId") or gateway_raw.get("app_id")) or "default",
|
||||
project_id=_string(gateway_raw.get("projectId") or gateway_raw.get("project_id")) or "default",
|
||||
scope=_string_list(gateway_raw.get("scope")) or ["current_chat", "resources"],
|
||||
scope=scope,
|
||||
top_k=8 if parsed_top_k is None else parsed_top_k,
|
||||
timeout_seconds=10.0 if parsed_timeout is None else parsed_timeout,
|
||||
)
|
||||
|
||||
@ -579,6 +579,29 @@ def test_hybrid_memory_rejects_unknown_scope(tmp_path) -> None:
|
||||
load_config(config_path=config_path)
|
||||
|
||||
|
||||
def test_hybrid_memory_rejects_empty_scope(tmp_path) -> None:
|
||||
config_path = tmp_path / "config.json"
|
||||
config_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"memory": {
|
||||
"mode": "hybrid",
|
||||
"gateway": {
|
||||
"baseUrl": "http://127.0.0.1:8010",
|
||||
"userId": "gateway-user",
|
||||
"userKey": "uk_secret",
|
||||
"scope": [],
|
||||
},
|
||||
}
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="scope"):
|
||||
load_config(config_path=config_path)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("gateway_override", "expected_error"),
|
||||
[
|
||||
|
||||
@ -68,7 +68,10 @@ def test_loader_implicit_hybrid_without_credentials_warns_and_degrades(
|
||||
loaded.close()
|
||||
|
||||
|
||||
def test_loader_explicit_hybrid_without_credentials_fails_without_secret(tmp_path) -> None:
|
||||
def test_loader_explicit_hybrid_without_credentials_fails_before_opening_session_store(
|
||||
tmp_path,
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
config = BeaverConfig(
|
||||
memory=MemoryConfig(
|
||||
mode="hybrid",
|
||||
@ -77,6 +80,11 @@ def test_loader_explicit_hybrid_without_credentials_fails_without_secret(tmp_pat
|
||||
)
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"beaver.engine.loader.SessionManager",
|
||||
lambda workspace: pytest.fail("session store opened before memory config validation"),
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
EngineLoader(workspace=tmp_path, config=config).load()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user