diff --git a/ga.py b/ga.py index d2b25aef..c6d4ccfd 100644 --- a/ga.py +++ b/ga.py @@ -400,6 +400,7 @@ class GenericAgentHandler(BaseHandler): with open(path, 'a' if mode == "append" else 'w', encoding="utf-8") as f: f.write(new_content) yield f"[Status] ✅ {mode.capitalize()} 成功 ({len(new_content)} bytes)\n" next_prompt = self._get_anchor_prompt(skip=args.get('_index', 0) > 0) + if len(new_content) > 5000: next_prompt = "\n[SYSTEM TIPS] WRITE TOO LONG! MUST RECHECK HALLUCINATIONS! SMALL WRITES OR PATCHES NEXT TIME!" return StepOutcome({"status": "success", 'writed_bytes': len(new_content)}, next_prompt=next_prompt) except Exception as e: yield f"[Status] ❌ 写入异常: {str(e)}\n" @@ -427,6 +428,7 @@ class GenericAgentHandler(BaseHandler): def export_history(self, fn): with open(fn, 'w', encoding='utf-8') as f: json.dump(self.parent.llmclient.backend.history, f, ensure_ascii=False) + def enter_project_mode(self, name): self.parent._ga_project_mode_name = name def _in_plan_mode(self): return self.working.get('in_plan_mode') def _exit_plan_mode(self): self.working.pop('in_plan_mode', None) def enter_plan_mode(self, plan_path): @@ -539,9 +541,9 @@ class GenericAgentHandler(BaseHandler): def _get_anchor_prompt(self, skip=False): if skip: return "\n" h = self.history_info; W = 30 - earlier = f'\n{self._fold_earlier(h[:-W])}\n\n' if len(h) > W else "" - h_str = "\n".join(h[-W:]) - prompt = f"\n### [WORKING MEMORY]\n{earlier}\n{h_str}\n" + earlier = f'\n{self._fold_earlier(h[:-W])}\n\n' if len(h) > W and self.current_turn % 4 == 1 else "" + history = f'\n{"\n".join(h[-W:])}\n' if self.current_turn % 2 == 1 else "" + prompt = f"\n### [WORKING MEMORY]\n{earlier}{history}" prompt += f"\nCurrent turn: {self.current_turn}\n" if self.working.get('key_info'): prompt += f"\n{self.working.get('key_info')}" if self.working.get('related_sop'): prompt += f"\n有不清晰的地方请再次读取{self.working.get('related_sop')}" diff --git a/memory/project_mode_sop.md b/memory/project_mode_sop.md index 86cc9f7f..0b4c0ae8 100644 --- a/memory/project_mode_sop.md +++ b/memory/project_mode_sop.md @@ -7,14 +7,14 @@ Project Mode = 跨会话保持项目认知的工作模式 ## 进入 -锚 = `./.active_project.<宿主pid>`,只对当前 GA 进程有效:GA 关闭即自动失活;多开 GA 各自激活不同项目,互不干扰。(下文路径一律以 cwd 为基准,cwd 即 GA 的 temp 目录;禁写 `temp/xxx` 前缀,会嵌套出 temp/temp) +激活态保存在当前 Agent 实例;各会话互不干扰,关闭 GA 自动失效。(下文路径以 cwd 为基准,禁写 `temp/xxx` 前缀) - 用户只说「进入项目模式」未指明项目:列出 `./projects/` 下各项目(名字 + memory 行数 + 最后修改时间),ask_user 让用户选定后再继续 - 用户明确说「进入/切换到 <项目名> 项目」:视为已确认,直接执行: 1. 建目录 `./projects/<项目名>/`,无则创建 `project_memory.md`(空文件即可) -2. 写文件锚,必须用 code_run(锚文件名含宿主 pid,ppid 即 GA 宿主进程): - `import os; open(f'./.active_project.{os.getppid()}', 'w').write('<项目名>')` +2. 用 `code_run` 的 `inline_eval=true` 绑定当前 Agent(`handler` 已注入,无需 import): + `handler.enter_project_mode('<项目名>')` 3. 回读 `project_memory.md` 全文,向用户复述项目现状 ## 期间纪律 @@ -25,6 +25,5 @@ Project Mode = 跨会话保持项目认知的工作模式 ## 离开 -用户表示「离开项目模式」时:删除 `./.active_project.<宿主pid>`(仅关闭激活态,项目目录与 `project_memory.md` 原样保留) -切换到另一项目时无需先离开,直接按「进入」覆盖文件锚即可 -GA 关闭不需任何操作:锚随进程消亡自动失效,残留文件由插件下次启动时清扫 +明确要求离开时,用 `code_run`(`inline_eval=true`)执行 `handler.enter_project_mode(None)`。 +切换项目直接换项目名 diff --git a/plugins/project_mode.py b/plugins/project_mode.py index cca29d32..3c58592c 100644 --- a/plugins/project_mode.py +++ b/plugins/project_mode.py @@ -6,11 +6,7 @@ 由模型按 L1 中的指针与线索(行数/大小)自行判断是否用 file 工具读取。 利用 messages 是 list 引用的事实,直接 mutate 即反映到真正发给 LLM 的内容。 -激活态载体 = 文件锚 temp/.active_project.<宿主pid>(存当前项目名)。PID 键控: - - 锚只对写它的那个 GA 进程有效 → 多开 GA 各自激活不同项目,互不可见 - - GA 关闭即自动失活(重启后 pid 变,旧锚作废);重新激活需经用户确认(SOP 指示) - - 进入:agent 读 project_mode_sop,经用户确认后写锚(code_run 中 os.getppid() 即宿主 pid) - - 退出:删除该文件。插件加载时清扫旧版无后缀锚与自己 pid 的前世残留(不碰他进程的锚) +激活态保存在当前 GenericAgent 实例的 _ga_project_mode_name;各会话互不干扰。 目录约定: temp/projects/<项目名>/project_memory.md 单文件全文注入的项目记忆 @@ -21,43 +17,13 @@ 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, f'.active_project.{os.getpid()}') - - -def _cleanup_stale_anchors(): - """清扫无主锚:仅删两类能纯靠文件名判定的——旧版无后缀的、自己 pid 的(刚启动 - 不可能写过锚,必是 pid 复用的前世残留)。他进程的锚一律不碰:故意不做存活探测, - os.kill(pid, 0) 在 Windows 上语义是终止进程而非探活,任何平台分支都不值得为 - 删除无害残留文件引入杀进程风险(激活判定只认自己 pid 的锚,残留不影响行为)。""" - import glob - for path in glob.glob(os.path.join(_TEMP, '.active_project*')): - pid = path.rsplit('.', 1)[-1] - if path != _ANCHOR and pid.isdigit(): - continue # 他进程的锚(含已死进程的),不碰 - try: os.remove(path) - except OSError: pass - - -_cleanup_stale_anchors() def _active_project(ctx=None): - """返回当前激活的项目名;未激活返回 None。 - - 兼容策略: - - 新 TUI 多会话可在当前 GenericAgent 实例上设置 _ga_project_mode_name。 - 只要该属性存在,就以它为准;值为 None/空串表示该 agent 普通模式。 - - 其它 UI / 旧 SOP 不设置该属性,继续读取 pid 键控文件锚。 - 异常不在此捕获——hooks.trigger 统一捕获并打印,保持可观测。""" - parent = None - if isinstance(ctx, dict): - handler = ctx.get('handler') - parent = getattr(handler, 'parent', None) - if parent is not None and hasattr(parent, '_ga_project_mode_name'): - return getattr(parent, '_ga_project_mode_name', None) or None - if not os.path.isfile(_ANCHOR): - return None - return open(_ANCHOR, encoding='utf-8').read().strip() or None + """返回当前 Agent 实例激活的项目名;未激活返回 None。""" + handler = ctx.get('handler') if isinstance(ctx, dict) else None + parent = getattr(handler, 'parent', None) + return getattr(parent, '_ga_project_mode_name', None) or None def _project_dir(name):