From 1945b27a03a6e52f39f39a143618f7ee6fcfbc16 Mon Sep 17 00:00:00 2001 From: weijia <126332148+wjl2023@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:18:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20project=20mode=20plugin=20=E2=80=94=20c?= =?UTF-8?q?ross-session=20project=20memory=20via=20two-layer=20injection?= =?UTF-8?q?=20(#591)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Activate a project with a file anchor (temp/.active_project); every turn the agent_before hook appends an L1 context (rules + memory file pointer + wrap-up discipline) to the latest user message, while the full project memory (L2) stays on disk for the model to read on demand. Zero core changes: plugin + SOP + gitignore whitelist only. --- .gitignore | 3 ++ memory/project_mode_sop.md | 24 +++++++++ plugins/project_mode.py | 99 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 memory/project_mode_sop.md create mode 100644 plugins/project_mode.py diff --git a/.gitignore b/.gitignore index a16c486e..85ceced4 100644 --- a/.gitignore +++ b/.gitignore @@ -146,3 +146,6 @@ frontends/conductor_im_plugins/config.local.json **/__pycache__/ .claude/ +# Project Mode SOP +!memory/project_mode_sop.md + diff --git a/memory/project_mode_sop.md b/memory/project_mode_sop.md new file mode 100644 index 00000000..e4494e4b --- /dev/null +++ b/memory/project_mode_sop.md @@ -0,0 +1,24 @@ +# Project Mode SOP + +## 定义 + +Project Mode = 跨会话保持项目认知的工作模式 +载体:`project_memory.md` + 项目私域目录。两层注入:每轮自动注入的只有 L1(规则+记忆文件指针),L2(`project_memory.md` 全文)不注入——任务涉及项目上下文时自己用 file 工具去读,无关则不读 + +## 进入 + +用户表示「进入/切换到 <项目名> 项目」时(下文路径一律以 cwd 为基准,cwd 即 GA 的 temp 目录;禁写 `temp/xxx` 前缀,会嵌套出 temp/temp): +1. 建目录 `./projects/<项目名>/`,无则创建 `project_memory.md`(空文件即可) +2. 写文件锚:把 `<项目名>` 写入 `./.active_project` +3. 回读 `project_memory.md` 全文,向用户复述项目现状 + +## 期间纪律 + +- 项目文件(todo、草稿、产物)一律放 `./projects/<项目名>/`,禁止丢 temp 根目录 +- 入库判据(唯一标准):每得到一条信息,自问「记忆归零、重新接手本项目的我,缺了这条会不会重复付出认知代价——再踩一次坑、再摸索一次、再问一次用户?」会则立即追加进 `project_memory.md`,不会则不记 +- 一条一句,写成未来的自己能直接复用的形式;已有条目增量更新,不整篇重写 + +## 离开 + +用户表示「离开项目模式」时:清空 `./.active_project`(仅关闭激活态,项目目录与 `project_memory.md` 原样保留) +切换到另一项目时无需先离开,直接按「进入」覆盖文件锚即可 diff --git a/plugins/project_mode.py b/plugins/project_mode.py new file mode 100644 index 00000000..c14d7e38 --- /dev/null +++ b/plugins/project_mode.py @@ -0,0 +1,99 @@ +"""Project Mode plugin — 零核心代码改动实现 GA 项目模式。 + +机制:注册 agent_before hook(agent_loop.py 中每个用户轮触发一次)。 +当项目模式激活时,把 L1 层(规则 + 记忆文件指针 + 收尾纪律)追加到最后一条 user message(str 直接拼接,多模态 list 追加 text block)。 +两层设计:L1 每轮全量注入(轻量、稳定);L2 = project_memory.md 全文不注入, +由模型按 L1 中的指针与线索(行数/大小)自行判断是否用 file 工具读取。 +利用 messages 是 list 引用的事实,直接 mutate 即反映到真正发给 LLM 的内容。 + +激活态载体 = 文件锚 temp/.active_project(存当前项目名)。文件驱动,跨轮天生持久。 + - 文件存在且非空 → 项目模式激活 + - 进入:agent 读 project_mode_sop 后写该文件(SOP 指示) + - 退出:清空/删除该文件 + +目录约定: + temp/projects/<项目名>/project_memory.md 单文件全文注入的项目记忆 + temp/projects/<项目名>/ 项目私域文件(todo 等),解决多项目覆盖 +""" +import os +import plugins.hooks as hooks + +_PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +_TEMP = os.path.join(_PROJECT_ROOT, 'temp') +_ANCHOR = os.path.join(_TEMP, '.active_project') + + +def _active_project(): + """读文件锚,返回当前激活的项目名;未激活返回 None。 + 异常不在此捕获——hooks.trigger 统一捕获并打印,保持可观测。""" + if not os.path.isfile(_ANCHOR): + return None + return open(_ANCHOR, encoding='utf-8').read().strip() or None + + +def _project_dir(name): + return os.path.join(_TEMP, 'projects', name) + + +def _mem_path(name): + return os.path.join(_project_dir(name), 'project_memory.md') + + +def _memory_stat(name): + """返回 project_memory.md 的 (存在, 行数, 字节数),供 L1 指针给模型判断依据。""" + path = _mem_path(name) + if os.path.isfile(path): + data = open(path, encoding='utf-8').read() + return True, len(data.splitlines()), len(data.encode('utf-8')) + return False, 0, 0 + + +def _build_injection(name): + """构造追加到 user message 末尾的内容(两层设计的 L1 层)。 + + L1(每轮全量注入):规范/规则/操作说明 + 记忆文件指针(含行数/大小线索)。 + L2(按需):project_memory.md 全文不注入,由模型自行判断是否用 file 工具去读。 + """ + pdir = _project_dir(name) + mem_path = _mem_path(name) + exists, lines, nbytes = _memory_stat(name) + if exists and nbytes > 0: + mem_hint = ( + f"项目全量记忆在 {mem_path}({lines} 行 / {nbytes} 字节)。" + f"本轮任务若涉及项目上下文(接续工作、查约定、避坑),先读它再动手;" + f"若与项目认知无关(闲聊、独立小事),可不读。自行判断。" + ) + else: + mem_hint = f"项目记忆 {mem_path} 暂为空(本项目尚无沉淀),无需读取。" + return ( + f"\n\n---\n" + f"[PROJECT MODE: {name}]\n" + f"你正在「{name}」项目模式中。\n\n" + f"## 规则\n" + f"- 项目私域目录:{pdir}(todo、草稿、产物一律放这里,勿放 temp 根目录)\n" + f"- {mem_hint}\n\n" + f"## 收尾纪律\n" + f"干完本轮活后自问一个问题:「记忆归零、重新接手本项目的我,缺了本轮哪条信息会重复付出认知代价" + f"——再踩一次坑、再摸索一次、再问一次用户?」会的,就用 file 工具把那条追加进 {mem_path}," + f"写成未来的自己能直接复用的一句话;不会的,一个字都不写。\n" + f"---" + ) + + +@hooks.register('agent_before') +def inject_project_context(ctx): + """每个用户轮起始时,若项目模式激活,把项目上下文追加到 user message。""" + name = _active_project() + if not name: + return # 未激活,普通模式,什么都不做 + + # 从尾部找最后一条 user message(不依赖 messages[1] 的位置约定) + um = next((m for m in reversed(ctx.get('messages') or []) + if isinstance(m, dict) and m.get('role') == 'user'), None) + if um is None: + return + content = um.get('content') + if isinstance(content, str): + um['content'] = content + _build_injection(name) + elif isinstance(content, list): # 多模态:追加 text block + content.append({'type': 'text', 'text': _build_injection(name)})