Current section

Files

Jump to
cmdc lib cmdc plugin builtin memory_loader.ex
Raw

lib/cmdc/plugin/builtin/memory_loader.ex

defmodule CMDC.Plugin.Builtin.MemoryLoader do
@moduledoc """
P1 持久记忆插件 — 对标 DeepAgents MemoryMiddleware,加载 AGENTS.md 并注入系统提示词。
## 工作原理
1. **`session_start`** — 从 `memory_files` 配置的路径(默认 `working_dir/AGENTS.md`)读取记忆文件。
通过 `emit {:memory_contents, %{file_name => content}}` 将内容传递给 Agent,
由 Agent 更新 `state.memory_contents`,再由 `SystemPrompt``<agent_memory>` 标签注入
每次 LLM 请求的 system prompt。
2. **`after_tool`** — 监控 `write_file`/`edit_file` 工具对记忆文件的写入,
自动更新 plugin state 中的缓存内容,并再次 emit 触发重新加载。
## `<agent_memory>` 注入格式
SystemPrompt 模块将 `memory_contents` 渲染为:
```xml
<agent_memory name="AGENTS.md">
...文件内容...
</agent_memory>
```
## 配置
{CMDC.Plugin.Builtin.MemoryLoader,
memory_files: ["AGENTS.md"], # 相对于 working_dir 的记忆文件路径列表
auto_reload: true # 工具写入记忆文件后自动重新加载
}
## emit 事件协议
- `{:memory_contents, %{String.t() => String.t()}}` — 记忆内容 map,key 为文件名
Agent 收到此 emit 后将更新 `state.memory_contents`,确保下一次 LLM 请求包含最新记忆。
"""
@behaviour CMDC.Plugin
require Logger
@default_memory_files ["AGENTS.md"]
# ==========================================================================
# Plugin Callbacks
# ==========================================================================
@impl true
def init(opts) do
state = %{
memory_files: Keyword.get(opts, :memory_files, @default_memory_files),
auto_reload: Keyword.get(opts, :auto_reload, true),
loaded_contents: %{}
}
{:ok, state}
end
@impl true
def priority, do: 100
@impl true
def describe, do: "P1 持久记忆:AGENTS.md 加载 → system prompt 注入 <agent_memory>"
@impl true
def handle_event(:session_start, state, ctx) do
contents = load_memory_files(state.memory_files, ctx.working_dir)
if map_size(contents) > 0 do
Logger.debug("[MemoryLoader] 加载 #{map_size(contents)} 个记忆文件 session=#{ctx.session_id}")
state = %{state | loaded_contents: contents}
{:emit, {:memory_contents, contents}, state}
else
{:continue, state}
end
end
def handle_event({:after_tool, tool_name, _call_id, {:ok, _}}, state, ctx)
when tool_name in ["write_file", "edit_file"] and state.auto_reload do
reload_if_memory_file(state, ctx)
end
def handle_event(_event, state, _ctx), do: {:continue, state}
# ==========================================================================
# 私有 — 记忆文件加载
# ==========================================================================
defp load_memory_files(file_names, working_dir) do
Enum.reduce(file_names, %{}, fn file_name, acc ->
path = resolve_path(file_name, working_dir)
case File.read(path) do
{:ok, content} when content != "" ->
Map.put(acc, file_name, content)
{:ok, _empty} ->
acc
{:error, _reason} ->
acc
end
end)
end
defp resolve_path(file_name, working_dir)
when is_binary(working_dir) and working_dir != "" do
if Path.type(file_name) == :absolute do
file_name
else
Path.join(working_dir, file_name)
end
end
defp resolve_path(file_name, _working_dir), do: file_name
# ==========================================================================
# 私有 — 工具写入后重新加载
# ==========================================================================
defp reload_if_memory_file(state, ctx) do
new_contents = load_memory_files(state.memory_files, ctx.working_dir)
old_contents = state.loaded_contents
if new_contents != old_contents do
changed_files = find_changed_files(old_contents, new_contents)
Logger.debug(
"[MemoryLoader] 记忆文件已变更,重新加载: #{inspect(changed_files)} session=#{ctx.session_id}"
)
state = %{state | loaded_contents: new_contents}
{:emit, {:memory_contents, new_contents}, state}
else
{:continue, state}
end
end
defp find_changed_files(old, new) do
all_keys = Map.keys(old) ++ Map.keys(new)
all_keys
|> Enum.uniq()
|> Enum.filter(fn k -> Map.get(old, k) != Map.get(new, k) end)
end
end