|
@@ -7,20 +7,31 @@ Dream:记忆反思操作(Phase 3)
|
|
|
用 dream_prompt 指导 LLM 更新记忆文件
|
|
用 dream_prompt 指导 LLM 更新记忆文件
|
|
|
|
|
|
|
|
对外入口:
|
|
对外入口:
|
|
|
- run_dream(store, llm_call, memory_config, trace_filter=None, model=...)
|
|
|
|
|
|
|
+ run_dream(store, llm_call, memory_config, dream_scope, model=...)
|
|
|
"""
|
|
"""
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
import json
|
|
import json
|
|
|
import logging
|
|
import logging
|
|
|
|
|
+import os
|
|
|
import re
|
|
import re
|
|
|
|
|
+import tempfile
|
|
|
from dataclasses import dataclass, field
|
|
from dataclasses import dataclass, field
|
|
|
from datetime import datetime
|
|
from datetime import datetime
|
|
|
from pathlib import Path
|
|
from pathlib import Path
|
|
|
from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple
|
|
from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple
|
|
|
|
|
|
|
|
-from cyber_agent.core.memory import MemoryConfig, load_memory_files, format_memory_injection
|
|
|
|
|
|
|
+from cyber_agent.core.memory import (
|
|
|
|
|
+ MEMORY_IDENTITY_CONTEXT_KEY,
|
|
|
|
|
+ MemoryConfig,
|
|
|
|
|
+ MemoryPathError,
|
|
|
|
|
+ compute_memory_identity,
|
|
|
|
|
+ format_memory_injection,
|
|
|
|
|
+ load_memory_files,
|
|
|
|
|
+ normalize_memory_relative_path,
|
|
|
|
|
+ resolve_memory_path,
|
|
|
|
|
+)
|
|
|
from cyber_agent.trace.models import Trace
|
|
from cyber_agent.trace.models import Trace
|
|
|
from cyber_agent.trace.store import FileSystemTraceStore
|
|
from cyber_agent.trace.store import FileSystemTraceStore
|
|
|
|
|
|
|
@@ -65,6 +76,42 @@ DEFAULT_DREAM_PROMPT = """你正在整理自己的长期记忆。下面是你最
|
|
|
|
|
|
|
|
# ===== 数据结构 =====
|
|
# ===== 数据结构 =====
|
|
|
|
|
|
|
|
|
|
+@dataclass(frozen=True)
|
|
|
|
|
+class DreamScope:
|
|
|
|
|
+ """Dream 可见 Trace 的完整身份边界。
|
|
|
|
|
+
|
|
|
|
|
+ uid 允许为 None,但仍按 None 做精确匹配;不会将其视为
|
|
|
|
|
+ “不过滤”。
|
|
|
|
|
+ """
|
|
|
|
|
+
|
|
|
|
|
+ uid: Optional[str]
|
|
|
|
|
+ agent_type: str
|
|
|
|
|
+ memory_identity: str
|
|
|
|
|
+
|
|
|
|
|
+ def __post_init__(self) -> None:
|
|
|
|
|
+ if not self.agent_type:
|
|
|
|
|
+ raise ValueError("DreamScope.agent_type is required")
|
|
|
|
|
+ if not self.memory_identity:
|
|
|
|
|
+ raise ValueError("DreamScope.memory_identity is required")
|
|
|
|
|
+
|
|
|
|
|
+ def matches(self, trace: Optional[Trace]) -> bool:
|
|
|
|
|
+ if trace is None:
|
|
|
|
|
+ return False
|
|
|
|
|
+ context = trace.context if isinstance(trace.context, dict) else {}
|
|
|
|
|
+ return (
|
|
|
|
|
+ trace.uid == self.uid
|
|
|
|
|
+ and trace.agent_type == self.agent_type
|
|
|
|
|
+ and context.get(MEMORY_IDENTITY_CONTEXT_KEY) == self.memory_identity
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+class DreamScopeError(ValueError):
|
|
|
|
|
+ """DreamScope 与 MemoryConfig 或 Trace 身份不一致。"""
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+class DreamPlanError(ValueError):
|
|
|
|
|
+ """LLM 产生的记忆更新计划不安全或不完整。"""
|
|
|
|
|
+
|
|
|
@dataclass
|
|
@dataclass
|
|
|
class DreamReport:
|
|
class DreamReport:
|
|
|
per_trace_summaries: Dict[str, str] = field(default_factory=dict) # {trace_id: summary}
|
|
per_trace_summaries: Dict[str, str] = field(default_factory=dict) # {trace_id: summary}
|
|
@@ -84,6 +131,7 @@ async def per_trace_reflect(
|
|
|
llm_call: LLMCall,
|
|
llm_call: LLMCall,
|
|
|
trace_id: str,
|
|
trace_id: str,
|
|
|
memory_config: MemoryConfig,
|
|
memory_config: MemoryConfig,
|
|
|
|
|
+ dream_scope: DreamScope,
|
|
|
model: str = "gpt-4o-mini",
|
|
model: str = "gpt-4o-mini",
|
|
|
) -> Optional[str]:
|
|
) -> Optional[str]:
|
|
|
"""为单个 trace 生成反思摘要,写入 cognition_log,更新 reflected_at_sequence。
|
|
"""为单个 trace 生成反思摘要,写入 cognition_log,更新 reflected_at_sequence。
|
|
@@ -92,8 +140,8 @@ async def per_trace_reflect(
|
|
|
反思摘要字符串;若 trace 没有新消息或 LLM 返回空,返回 None。
|
|
反思摘要字符串;若 trace 没有新消息或 LLM 返回空,返回 None。
|
|
|
"""
|
|
"""
|
|
|
trace = await store.get_trace(trace_id)
|
|
trace = await store.get_trace(trace_id)
|
|
|
- if not trace:
|
|
|
|
|
- logger.debug(f"[Dream] trace 不存在: {trace_id}")
|
|
|
|
|
|
|
+ if not dream_scope.matches(trace):
|
|
|
|
|
+ logger.warning(f"[Dream] trace 超出 DreamScope,拒绝反思: {trace_id}")
|
|
|
return None
|
|
return None
|
|
|
|
|
|
|
|
start_seq = (trace.reflected_at_sequence or 0) + 1
|
|
start_seq = (trace.reflected_at_sequence or 0) + 1
|
|
@@ -198,21 +246,17 @@ async def cross_trace_integrate(
|
|
|
store: FileSystemTraceStore,
|
|
store: FileSystemTraceStore,
|
|
|
llm_call: LLMCall,
|
|
llm_call: LLMCall,
|
|
|
memory_config: MemoryConfig,
|
|
memory_config: MemoryConfig,
|
|
|
- trace_filter: Optional[Callable[[Trace], bool]] = None,
|
|
|
|
|
|
|
+ dream_scope: DreamScope,
|
|
|
model: str = "gpt-4o",
|
|
model: str = "gpt-4o",
|
|
|
) -> Tuple[int, List[str], str]:
|
|
) -> Tuple[int, List[str], str]:
|
|
|
"""汇总各 trace 未消化的 reflection 事件,用 LLM 更新记忆文件。
|
|
"""汇总各 trace 未消化的 reflection 事件,用 LLM 更新记忆文件。
|
|
|
|
|
|
|
|
- Args:
|
|
|
|
|
- trace_filter: 可选的 trace 过滤函数(例如按 agent_type / owner);
|
|
|
|
|
- None 表示扫描 TraceStore 下所有 trace。
|
|
|
|
|
-
|
|
|
|
|
Returns:
|
|
Returns:
|
|
|
(consumed_reflection_count, updated_file_paths, reasoning)
|
|
(consumed_reflection_count, updated_file_paths, reasoning)
|
|
|
"""
|
|
"""
|
|
|
|
|
+ _validate_dream_scope(memory_config, dream_scope)
|
|
|
all_traces = await store.list_traces(limit=1000)
|
|
all_traces = await store.list_traces(limit=1000)
|
|
|
- if trace_filter:
|
|
|
|
|
- all_traces = [t for t in all_traces if trace_filter(t)]
|
|
|
|
|
|
|
+ all_traces = [t for t in all_traces if dream_scope.matches(t)]
|
|
|
|
|
|
|
|
# 收集所有未消化的 reflection 事件
|
|
# 收集所有未消化的 reflection 事件
|
|
|
reflections: List[Tuple[str, Dict[str, Any]]] = [] # [(trace_id, event)]
|
|
reflections: List[Tuple[str, Dict[str, Any]]] = [] # [(trace_id, event)]
|
|
@@ -229,8 +273,6 @@ async def cross_trace_integrate(
|
|
|
|
|
|
|
|
# 读当前记忆文件
|
|
# 读当前记忆文件
|
|
|
existing_files = load_memory_files(memory_config)
|
|
existing_files = load_memory_files(memory_config)
|
|
|
- existing_by_path = {rel: (purpose, content) for rel, purpose, content in existing_files}
|
|
|
|
|
-
|
|
|
|
|
user_content = _build_dream_input(reflections, existing_files, memory_config)
|
|
user_content = _build_dream_input(reflections, existing_files, memory_config)
|
|
|
prompt = memory_config.dream_prompt or DEFAULT_DREAM_PROMPT
|
|
prompt = memory_config.dream_prompt or DEFAULT_DREAM_PROMPT
|
|
|
|
|
|
|
@@ -254,42 +296,209 @@ async def cross_trace_integrate(
|
|
|
logger.error(f"[Dream] LLM 输出无法解析为 JSON 计划,原文: {raw[:500]}")
|
|
logger.error(f"[Dream] LLM 输出无法解析为 JSON 计划,原文: {raw[:500]}")
|
|
|
return 0, [], ""
|
|
return 0, [], ""
|
|
|
|
|
|
|
|
- updated_paths: List[str] = []
|
|
|
|
|
- base = Path(memory_config.base_path)
|
|
|
|
|
|
|
+ try:
|
|
|
|
|
+ prepared_updates = _prepare_memory_updates(memory_config, plan)
|
|
|
|
|
+ except (DreamPlanError, MemoryPathError) as exc:
|
|
|
|
|
+ logger.error(f"[Dream] 拒绝不安全的记忆更新计划: {exc}")
|
|
|
|
|
+ return 0, [], ""
|
|
|
|
|
|
|
|
- for update in plan.get("updates", []):
|
|
|
|
|
- rel_path = update.get("path", "")
|
|
|
|
|
- new_content = update.get("new_content", "")
|
|
|
|
|
- if not rel_path:
|
|
|
|
|
- continue
|
|
|
|
|
- # 安全检查:禁止路径穿越
|
|
|
|
|
- target = (base / rel_path).resolve()
|
|
|
|
|
- if not str(target).startswith(str(base.resolve())):
|
|
|
|
|
- logger.warning(f"[Dream] 拒绝写入 base_path 之外的路径: {rel_path}")
|
|
|
|
|
- continue
|
|
|
|
|
- target.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
- target.write_text(new_content, encoding="utf-8")
|
|
|
|
|
- updated_paths.append(rel_path)
|
|
|
|
|
- logger.info(f"[Dream] 已更新记忆文件: {rel_path} ({len(new_content)} chars)")
|
|
|
|
|
|
|
+ # LLM 返回后再次检查 Trace 身份,防止在长耗时调用期间边界变化。
|
|
|
|
|
+ for trace_id in {trace_id for trace_id, _ in reflections}:
|
|
|
|
|
+ if not dream_scope.matches(await store.get_trace(trace_id)):
|
|
|
|
|
+ raise DreamScopeError(
|
|
|
|
|
+ f"trace left DreamScope before memory update: {trace_id}"
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
- # 标记所有参与的 reflection 为已消化
|
|
|
|
|
|
|
+ updated_paths = _transactional_write_memory_updates(
|
|
|
|
|
+ memory_config,
|
|
|
|
|
+ prepared_updates,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # 只有全部记忆文件原子替换成功后,才标记 reflection 已消化。
|
|
|
consumed_at = datetime.now().isoformat()
|
|
consumed_at = datetime.now().isoformat()
|
|
|
|
|
+ reflections_by_trace: Dict[str, List[Dict[str, Any]]] = {}
|
|
|
for trace_id, event in reflections:
|
|
for trace_id, event in reflections:
|
|
|
- log = await store.get_cognition_log(trace_id)
|
|
|
|
|
- events = log.get("events", log.get("entries", []))
|
|
|
|
|
- target_ts = event.get("timestamp")
|
|
|
|
|
- for e in events:
|
|
|
|
|
- if (
|
|
|
|
|
- e.get("type") == "reflection"
|
|
|
|
|
- and not e.get("consumed_at")
|
|
|
|
|
- and e.get("timestamp") == target_ts
|
|
|
|
|
- ):
|
|
|
|
|
- e["consumed_at"] = consumed_at
|
|
|
|
|
- log_file = store._get_cognition_log_file(trace_id)
|
|
|
|
|
- log_file.write_text(json.dumps(log, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
|
|
|
|
|
+ reflections_by_trace.setdefault(trace_id, []).append(event)
|
|
|
|
|
+ consumed_count = 0
|
|
|
|
|
+ for trace_id, trace_reflections in reflections_by_trace.items():
|
|
|
|
|
+ consumed_count += await store.mark_reflections_consumed(
|
|
|
|
|
+ trace_id,
|
|
|
|
|
+ trace_reflections,
|
|
|
|
|
+ consumed_at,
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
reasoning = plan.get("reasoning", "")
|
|
reasoning = plan.get("reasoning", "")
|
|
|
- return len(reflections), updated_paths, reasoning
|
|
|
|
|
|
|
+ return consumed_count, updated_paths, reasoning
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _validate_dream_scope(
|
|
|
|
|
+ memory_config: MemoryConfig,
|
|
|
|
|
+ dream_scope: DreamScope,
|
|
|
|
|
+) -> None:
|
|
|
|
|
+ expected_identity = compute_memory_identity(memory_config)
|
|
|
|
|
+ if dream_scope.memory_identity != expected_identity:
|
|
|
|
|
+ raise DreamScopeError(
|
|
|
|
|
+ "DreamScope.memory_identity does not match the active MemoryConfig"
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _prepare_memory_updates(
|
|
|
|
|
+ memory_config: MemoryConfig,
|
|
|
|
|
+ plan: Dict[str, Any],
|
|
|
|
|
+) -> List[Tuple[str, Path, str]]:
|
|
|
|
|
+ updates = plan.get("updates")
|
|
|
|
|
+ if not isinstance(updates, list):
|
|
|
|
|
+ raise DreamPlanError("updates must be a list")
|
|
|
|
|
+ prepared: List[Tuple[str, Path, str]] = []
|
|
|
|
|
+ seen: set[str] = set()
|
|
|
|
|
+ for index, update in enumerate(updates):
|
|
|
|
|
+ if not isinstance(update, dict):
|
|
|
|
|
+ raise DreamPlanError(f"updates[{index}] must be an object")
|
|
|
|
|
+ rel_path = update.get("path")
|
|
|
|
|
+ new_content = update.get("new_content")
|
|
|
|
|
+ if not isinstance(rel_path, str) or not isinstance(new_content, str):
|
|
|
|
|
+ raise DreamPlanError(
|
|
|
|
|
+ f"updates[{index}] requires string path and new_content"
|
|
|
|
|
+ )
|
|
|
|
|
+ normalized = normalize_memory_relative_path(rel_path)
|
|
|
|
|
+ if normalized != rel_path:
|
|
|
|
|
+ raise DreamPlanError(f"path is not normalized: {rel_path!r}")
|
|
|
|
|
+ if normalized in seen:
|
|
|
|
|
+ raise DreamPlanError(f"duplicate update path: {normalized}")
|
|
|
|
|
+ seen.add(normalized)
|
|
|
|
|
+ prepared.append(
|
|
|
|
|
+ (normalized, resolve_memory_path(memory_config, normalized), new_content)
|
|
|
|
|
+ )
|
|
|
|
|
+ return prepared
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _atomic_write_text(target: Path, content: str) -> None:
|
|
|
|
|
+ """在同目录写临时文件、fsync,然后原子替换目标。"""
|
|
|
|
|
+ tmp_path: Optional[Path] = None
|
|
|
|
|
+ try:
|
|
|
|
|
+ with tempfile.NamedTemporaryFile(
|
|
|
|
|
+ mode="w",
|
|
|
|
|
+ encoding="utf-8",
|
|
|
|
|
+ dir=str(target.parent),
|
|
|
|
|
+ prefix=f".{target.name}.",
|
|
|
|
|
+ suffix=".tmp",
|
|
|
|
|
+ delete=False,
|
|
|
|
|
+ ) as tmp:
|
|
|
|
|
+ tmp.write(content)
|
|
|
|
|
+ tmp.flush()
|
|
|
|
|
+ os.fsync(tmp.fileno())
|
|
|
|
|
+ tmp_path = Path(tmp.name)
|
|
|
|
|
+ os.replace(tmp_path, target)
|
|
|
|
|
+ tmp_path = None
|
|
|
|
|
+ try:
|
|
|
|
|
+ directory_fd = os.open(str(target.parent), os.O_RDONLY)
|
|
|
|
|
+ try:
|
|
|
|
|
+ os.fsync(directory_fd)
|
|
|
|
|
+ finally:
|
|
|
|
|
+ os.close(directory_fd)
|
|
|
|
|
+ except OSError:
|
|
|
|
|
+ # 某些文件系统不支持对目录 fsync;文件本身仍已 fsync + replace。
|
|
|
|
|
+ logger.debug(f"[Dream] 目录 fsync 不可用: {target.parent}")
|
|
|
|
|
+ finally:
|
|
|
|
|
+ if tmp_path is not None:
|
|
|
|
|
+ try:
|
|
|
|
|
+ tmp_path.unlink(missing_ok=True)
|
|
|
|
|
+ except OSError:
|
|
|
|
|
+ logger.warning(f"[Dream] 清理临时文件失败: {tmp_path}")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _stage_text(target: Path, content: str) -> Path:
|
|
|
|
|
+ """Durably stage one replacement next to its target without publishing it."""
|
|
|
|
|
+ target.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
+ with tempfile.NamedTemporaryFile(
|
|
|
|
|
+ mode="w",
|
|
|
|
|
+ encoding="utf-8",
|
|
|
|
|
+ dir=str(target.parent),
|
|
|
|
|
+ prefix=f".{target.name}.",
|
|
|
|
|
+ suffix=".dream-stage",
|
|
|
|
|
+ delete=False,
|
|
|
|
|
+ ) as staged:
|
|
|
|
|
+ staged.write(content)
|
|
|
|
|
+ staged.flush()
|
|
|
|
|
+ os.fsync(staged.fileno())
|
|
|
|
|
+ return Path(staged.name)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _transactional_write_memory_updates(
|
|
|
|
|
+ memory_config: MemoryConfig,
|
|
|
|
|
+ prepared_updates: List[Tuple[str, Path, str]],
|
|
|
|
|
+) -> List[str]:
|
|
|
|
|
+ """Publish a multi-file Dream plan as one recoverable in-process unit.
|
|
|
|
|
+
|
|
|
|
|
+ Filesystems do not provide a native multi-file rename transaction. We
|
|
|
|
|
+ therefore stage every new value first and keep every old value until all
|
|
|
|
|
+ replacements succeed. A later replacement failure rolls already-published
|
|
|
|
|
+ files back before the exception escapes, so reflections remain retryable
|
|
|
|
|
+ without exposing a partially committed plan.
|
|
|
|
|
+ """
|
|
|
|
|
+ staged_updates: List[Tuple[str, Path, str, Path]] = []
|
|
|
|
|
+ old_values: Dict[Path, Optional[str]] = {}
|
|
|
|
|
+ committed: List[Path] = []
|
|
|
|
|
+ try:
|
|
|
|
|
+ for rel_path, target, new_content in prepared_updates:
|
|
|
|
|
+ target.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
+ checked_target = resolve_memory_path(memory_config, rel_path)
|
|
|
|
|
+ if checked_target != target:
|
|
|
|
|
+ raise MemoryPathError(
|
|
|
|
|
+ f"memory target changed before staging: {rel_path}"
|
|
|
|
|
+ )
|
|
|
|
|
+ old_values[target] = (
|
|
|
|
|
+ target.read_text(encoding="utf-8") if target.exists() else None
|
|
|
|
|
+ )
|
|
|
|
|
+ staged_updates.append(
|
|
|
|
|
+ (rel_path, target, new_content, _stage_text(target, new_content))
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ for rel_path, target, new_content, staged_path in staged_updates:
|
|
|
|
|
+ checked_target = resolve_memory_path(memory_config, rel_path)
|
|
|
|
|
+ if checked_target != target:
|
|
|
|
|
+ raise MemoryPathError(
|
|
|
|
|
+ f"memory target changed before commit: {rel_path}"
|
|
|
|
|
+ )
|
|
|
|
|
+ os.replace(staged_path, target)
|
|
|
|
|
+ committed.append(target)
|
|
|
|
|
+ try:
|
|
|
|
|
+ directory_fd = os.open(str(target.parent), os.O_RDONLY)
|
|
|
|
|
+ try:
|
|
|
|
|
+ os.fsync(directory_fd)
|
|
|
|
|
+ finally:
|
|
|
|
|
+ os.close(directory_fd)
|
|
|
|
|
+ except OSError:
|
|
|
|
|
+ logger.debug("[Dream] 目录 fsync 不可用: %s", target.parent)
|
|
|
|
|
+ logger.info(
|
|
|
|
|
+ "[Dream] 已更新记忆文件: %s (%d chars)",
|
|
|
|
|
+ rel_path,
|
|
|
|
|
+ len(new_content),
|
|
|
|
|
+ )
|
|
|
|
|
+ return [rel_path for rel_path, *_ in staged_updates]
|
|
|
|
|
+ except Exception as commit_error:
|
|
|
|
|
+ rollback_errors: List[str] = []
|
|
|
|
|
+ for target in reversed(committed):
|
|
|
|
|
+ try:
|
|
|
|
|
+ old_content = old_values[target]
|
|
|
|
|
+ if old_content is None:
|
|
|
|
|
+ target.unlink(missing_ok=True)
|
|
|
|
|
+ else:
|
|
|
|
|
+ _atomic_write_text(target, old_content)
|
|
|
|
|
+ except Exception as rollback_error: # pragma: no cover - fatal FS failure
|
|
|
|
|
+ rollback_errors.append(f"{target}: {rollback_error}")
|
|
|
|
|
+ if rollback_errors:
|
|
|
|
|
+ raise RuntimeError(
|
|
|
|
|
+ "Dream update failed and rollback was incomplete: "
|
|
|
|
|
+ + "; ".join(rollback_errors)
|
|
|
|
|
+ ) from commit_error
|
|
|
|
|
+ raise
|
|
|
|
|
+ finally:
|
|
|
|
|
+ for _rel_path, _target, _content, staged_path in staged_updates:
|
|
|
|
|
+ try:
|
|
|
|
|
+ staged_path.unlink(missing_ok=True)
|
|
|
|
|
+ except OSError:
|
|
|
|
|
+ logger.warning("[Dream] 清理 staged 文件失败: %s", staged_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
def _build_dream_input(
|
|
def _build_dream_input(
|
|
@@ -342,15 +551,14 @@ async def run_dream(
|
|
|
store: FileSystemTraceStore,
|
|
store: FileSystemTraceStore,
|
|
|
llm_call: LLMCall,
|
|
llm_call: LLMCall,
|
|
|
memory_config: MemoryConfig,
|
|
memory_config: MemoryConfig,
|
|
|
- trace_filter: Optional[Callable[[Trace], bool]] = None,
|
|
|
|
|
|
|
+ dream_scope: DreamScope,
|
|
|
reflect_model: str = "gpt-4o-mini",
|
|
reflect_model: str = "gpt-4o-mini",
|
|
|
dream_model: str = "gpt-4o",
|
|
dream_model: str = "gpt-4o",
|
|
|
) -> DreamReport:
|
|
) -> DreamReport:
|
|
|
"""执行完整的 dream 流程:per_trace_reflect → cross_trace_integrate。
|
|
"""执行完整的 dream 流程:per_trace_reflect → cross_trace_integrate。
|
|
|
|
|
|
|
|
Args:
|
|
Args:
|
|
|
- trace_filter: 筛选需要反思的 trace(例如按 agent_type 或 owner);
|
|
|
|
|
- None 表示扫描所有 trace
|
|
|
|
|
|
|
+ dream_scope: 必填的 uid + agent_type + memory_identity 精确边界
|
|
|
reflect_model: per-trace 反思用的模型(轻量模型即可)
|
|
reflect_model: per-trace 反思用的模型(轻量模型即可)
|
|
|
dream_model: 跨 trace 整合用的模型(需要更强推理能力)
|
|
dream_model: 跨 trace 整合用的模型(需要更强推理能力)
|
|
|
"""
|
|
"""
|
|
@@ -359,18 +567,23 @@ async def run_dream(
|
|
|
if not memory_config.base_path:
|
|
if not memory_config.base_path:
|
|
|
logger.warning("[Dream] memory_config.base_path 未配置,跳过")
|
|
logger.warning("[Dream] memory_config.base_path 未配置,跳过")
|
|
|
return report
|
|
return report
|
|
|
|
|
+ _validate_dream_scope(memory_config, dream_scope)
|
|
|
|
|
|
|
|
# Phase 1: per-trace reflect
|
|
# Phase 1: per-trace reflect
|
|
|
all_traces = await store.list_traces(limit=1000)
|
|
all_traces = await store.list_traces(limit=1000)
|
|
|
- if trace_filter:
|
|
|
|
|
- all_traces = [t for t in all_traces if trace_filter(t)]
|
|
|
|
|
|
|
+ all_traces = [t for t in all_traces if dream_scope.matches(t)]
|
|
|
|
|
|
|
|
for t in all_traces:
|
|
for t in all_traces:
|
|
|
if (t.reflected_at_sequence or 0) >= t.last_sequence:
|
|
if (t.reflected_at_sequence or 0) >= t.last_sequence:
|
|
|
continue
|
|
continue
|
|
|
try:
|
|
try:
|
|
|
summary = await per_trace_reflect(
|
|
summary = await per_trace_reflect(
|
|
|
- store, llm_call, t.trace_id, memory_config, model=reflect_model,
|
|
|
|
|
|
|
+ store,
|
|
|
|
|
+ llm_call,
|
|
|
|
|
+ t.trace_id,
|
|
|
|
|
+ memory_config,
|
|
|
|
|
+ dream_scope,
|
|
|
|
|
+ model=reflect_model,
|
|
|
)
|
|
)
|
|
|
if summary:
|
|
if summary:
|
|
|
report.per_trace_summaries[t.trace_id] = summary
|
|
report.per_trace_summaries[t.trace_id] = summary
|
|
@@ -382,7 +595,7 @@ async def run_dream(
|
|
|
try:
|
|
try:
|
|
|
consumed, updated, reasoning = await cross_trace_integrate(
|
|
consumed, updated, reasoning = await cross_trace_integrate(
|
|
|
store, llm_call, memory_config,
|
|
store, llm_call, memory_config,
|
|
|
- trace_filter=trace_filter, model=dream_model,
|
|
|
|
|
|
|
+ dream_scope=dream_scope, model=dream_model,
|
|
|
)
|
|
)
|
|
|
report.consumed_reflection_count = consumed
|
|
report.consumed_reflection_count = consumed
|
|
|
report.updated_files = updated
|
|
report.updated_files = updated
|