diff --git a/.gitignore b/.gitignore index 58eae99d..4c56adcf 100644 --- a/.gitignore +++ b/.gitignore @@ -106,6 +106,10 @@ memory/L4_raw_sessions/* # Memory management !memory/memory_cleanup_sop.md +# Checklist/MapReduce +!memory/checklist_helper.py +!memory/checklist_sop.md + # Code Review Principles !memory/code_review_principles.md @@ -128,6 +132,7 @@ reflect/* !reflect/scheduler.py !reflect/agent_team_worker.py !reflect/goal_mode.py +!reflect/checklist_master.py # Universal: never track __pycache__ anywhere **/__pycache__/ diff --git a/memory/checklist_helper.py b/memory/checklist_helper.py new file mode 100644 index 00000000..31df5a87 --- /dev/null +++ b/memory/checklist_helper.py @@ -0,0 +1,88 @@ +# checklist_helper.py — CL(folder) 一站式任务清单(支持 checklist/mapreduce 两种模式) +import json, time, subprocess, socket, sys +from pathlib import Path +_R = Path(__file__).resolve().parent.parent +_BBS, _MAIN = _R/"assets/agent_bbs.py", _R/"agentmain.py" +_W_RE, _M_RE = _R/"reflect/agent_team_worker.py", _R/"reflect/checklist_master.py" +_PK = {"stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL} +if sys.platform == "win32": _PK["creationflags"] = 0x200 + +class CL: + def __init__(self, folder, goal="", workers=0): + """ + workers=0: checklist模式,master自己逐个执行,不启动BBS + workers>0: mapreduce模式,启动BBS+N个worker并行 + """ + self.folder = Path(folder); self.folder.mkdir(parents=True, exist_ok=True) + self.path = self.folder / "state.json" + self.workers = workers + if self.path.exists(): self._d = json.loads(self.path.read_text("utf-8")) + else: + self._d = {"closed": False, "goal": goal, "bbs": None, "tasks": []} + self._save() + if workers > 0: + self._ensure_bbs() + self.start_worker(workers) + + @property + def tasks(self): return self._d["tasks"] + @property + def closed(self): return self._d.get("closed", False) + @property + def has_open(self): return any(t["result"] is None for t in self.tasks) + @property + def bbs_url(self): return self._d["bbs"]["url"] if self._d["bbs"] else None + @property + def bbs_key(self): return self._d["bbs"]["key"] if self._d["bbs"] else None + @property + def mode(self): return "mapreduce" if self._d["bbs"] else "checklist" + def _save(self): self.path.write_text(json.dumps(self._d, ensure_ascii=False, indent=1), "utf-8") + + def _ensure_bbs(self): + if self._d["bbs"]: return + with socket.socket() as s: s.bind(('',0)); port = s.getsockname()[1] + key = f"cl_{int(time.time())%1000}" + (self.folder/"bbs").mkdir(exist_ok=True) + subprocess.Popen(["python", str(_BBS), "--cwd", str(self.folder/"bbs"), + "--port", str(port), "--key", key], **_PK) + time.sleep(1) + self._d["bbs"] = {"url": f"http://127.0.0.1:{port}", "key": key} + self._save() + + def add(self, texts): + nid = max((t["id"] for t in self.tasks), default=0) + 1 + ids = [] + for t in texts: + self.tasks.append({"id": nid, "text": t, "result": None, "ts": int(time.time())}) + ids.append(nid); nid += 1 + self._save(); return ids + + def mark(self, tid, result): + for t in self.tasks: + if t["id"] == tid: t["result"] = result; t["ts"] = int(time.time()); break + self._save() + + def look(self): + done = sum(1 for t in self.tasks if t["result"] is not None) + lines = [f"[{done}/{len(self.tasks)}] mode={self.mode}"] + for t in self.tasks: + l = f'{"✓" if t["result"] else "○"} #{t["id"]} {t["text"][:60]}' + if t["result"]: l += f' → {t["result"][:60]}' + lines.append(l) + return "\n".join(lines) + + def close(self): + assert not self.has_open, "has open tasks" + self._d["closed"] = True; self._save() + + def start_worker(self, n=None): + n = n or self.workers or 1 + if n <= 0: return + for i in range(n): + subprocess.Popen(["python", str(_MAIN), "--reflect", str(_W_RE), + "--base_url", self.bbs_url, "--board_key", self.bbs_key, "--name", f"w{i+1}"], **_PK) + if i < n - 1: time.sleep(5) + + def start_master(self): + subprocess.Popen(["python", str(_MAIN), "--reflect", str(_M_RE), + "--mr_folder", str(self.folder.resolve())], **_PK) diff --git a/memory/checklist_sop.md b/memory/checklist_sop.md new file mode 100644 index 00000000..e7cbaa42 --- /dev/null +++ b/memory/checklist_sop.md @@ -0,0 +1,97 @@ +# Checklist SOP + +## Booter(启动者/用户) + +**Checklist 模式**(单人,master自己执行): +```python +from checklist_helper import CL +cl = CL("cl_xxx", goal="<用户要求任务,尽量原样>") +cl.start_master() +``` + +**MapReduce 模式**(多人,master派发+worker执行): +```python +from checklist_helper import CL +cl = CL("cl_xxx", goal="<用户要求任务,尽量原样>", workers=2) +cl.start_master() +``` + +goal 写法:只写「做什么 + 参考哪个SOP」,不写怎么做。Master 自己读 SOP 决定 plan。 + +## Master(reflect agent 使用) + +```python +from checklist_helper import CL +cl = CL("cl_xxx") # 加载状态(BBS已在跑) +cl.add(["任务1", "任务2"]) # 在你的笔记中记录TODO项 +cl.look() # 查进度 +cl.mark(id, "摘要") # 验收 +cl.close() # 全部完成后关闭 +``` + +## Master plan示例 + +目标可分解为多个**不相干、可并行**的子任务 → add 子任务。 +B 要等 A 的结果 → 不要硬拆,串行做。 + +任务使用短句,派发时再补充信息。 + +1. 下载网盘 /game 下所有文件 + → 先 webscan 拿文件列表,再每个文件一条任务 + `cl.add(["下载A.exe", "下载B.zip", "下载C.zip"])` + +2. 从语法、风格、格式角度检查 a.pdf + → 三个维度天然独立 + `cl.add(["检查语法", "检查风格", "检查格式"])` + +3. 查所有 VPS 中版本 < 22 的,升级到 24 + → 第一轮:每台一条查版本任务 + `cl.add(["查 node03 版本", "查 node09 版本", "查 Dell 版本"])` + → reduce:master 筛出 < 22 的 + → 第二轮:每台需升级的一条任务 + `cl.add(["升级 node03 到 24", "升级 Dell 到 24"])` + +## Master 循环 + +``` +cl.look() +├─ 有未完成任务 → 去 BBS 派发(mapreduce模式)/ 挑最简单的**不派发**自己干 / 自己干(无worker checklist模式) +└─ 全部完成 + ├─ 用户最终目标已达成 → close() + └─ 最终目标未达成 → plan 下一步 + ├─ 可解耦 → add() 新一批任务 + ├─ 需串行前置 → 自己做一步,再回 look + └─ 基本搞定 → 自己整合结果,交付最终报告 +``` + +master会被持续唤醒直到其显式成功调用close()。 + +## 派发任务(有workers模式下) + +worker无法看到add的任务,只能看到BBS! +每条任务 prompt 须**自包含**——worker 没有 master 的上下文。 +每次最多只派发3个任务,不要一次性把所有任务贴到bbs上。 +worker足够聪明,只允许写目标和需要的信息,不要干预 +**master不允许执行已经派发出去的任务,会导致重复执行!** + +写 prompt 要点: +1. **背景**:worker 需要的信息直接给(路径、数据、约定),不要假设 worker 知道 +2. **交付物**:明确产出什么、格式、写到哪里 +3. **不限手段**:说要什么结果,别规定怎么做 +4. **不干预 BBS 行为**:禁止教 worker 如何抢单/回帖/报告,那是 worker 自己的机制 + +交付规范(写进任务 prompt): +- 交付结果和报告信息必须分开。交付 = 纯成品;报告 = 过程/问题/备注 +- 交付文件禁止出现说明性废话 +- 长结果写文件,短结果直接回帖 + +## 验收 + +Master 收到 worker 回帖或自己完成子任务后: +- 检查结果,语义判断 pass/fail → `cl.mark(id, "结果摘要")` +- 交付物含过程废话 → 要求重写交付物 +- 失败 → 可重发、换 prompt、或自己补 + +## 注意 + +- 若子任务需要 web 工具,提醒并行 worker 新建 tab 并使用自己的 tab diff --git a/reflect/checklist_master.py b/reflect/checklist_master.py new file mode 100644 index 00000000..8bb5dc88 --- /dev/null +++ b/reflect/checklist_master.py @@ -0,0 +1,65 @@ +import json, time, random +from pathlib import Path +from urllib import request + +INTERVAL = 60 +ONCE = False +_folder = None +_last_post_id = -1 + +def init(a): + global _folder + _folder = Path(a.get('mr_folder', '')) + +def _load(): + p = _folder / "state.json" + if not p.exists(): return None + return json.loads(p.read_text("utf-8")) + +def _poll_bbs(data): + global _last_post_id + bbs = data.get("bbs") + if not bbs: return [] + url, key = bbs.get("url", ""), bbs.get("key", "") + if not url: return [] + try: + req = request.Request(f"{url}/posts?limit=20&key={key}") + posts = json.loads(request.urlopen(req, timeout=10).read()) + if not posts: return [] + new = [p for p in posts if p['id'] > _last_post_id] + _last_post_id = max(p['id'] for p in posts) + return new + except Exception: + return [] + +def check(): + if not _folder: return '/exit' + data = _load() + if not data or data.get("closed"): return '/exit' + bbs = data.get("bbs") + if not bbs: return _prompt(data, []) + # mapreduce: 轮询BBS + new_posts = _poll_bbs(data) + tasks = data.get("tasks", []) + has_open = any(t["result"] is None for t in tasks) + if new_posts and has_open: return _prompt(data, new_posts) + if not has_open and (not tasks or random.random() < 0.2): return _prompt(data, new_posts) + return None + +def _prompt(data, new_posts): + bbs = data.get("bbs") + goal = data.get("goal", "") + mode = "mapreduce" if bbs else "checklist" + if new_posts: + trigger = "有新回帖,去BBS查看并验收" + elif any(t["result"] is None for t in data.get("tasks", [])): + trigger = "有未完成任务,继续执行" if not bbs else "有未完成任务,派发或自己执行" + else: + trigger = "无未完成任务,该plan下一步了" + lines = [f"你是 Checklist Master({mode}模式)。阅读 checklist_sop.md 按 Master 循环行事。"] + if bbs: + lines.append(f"BBS API文档(requests): GET {bbs['url']}/readme?key={bbs['key']}") + lines.append(f"目标: {goal}") + lines.append(f"唤醒原因: {trigger}") + lines.append(f'用 checklist_helper 的 CL("{_folder}") 管理状态(look/add/mark/close)。按决策树行动。') + return "\n".join(lines)