Skip to content

Commit e87f9e7

Browse files
committed
refactor: rename mapreduce -> checklist (helper/master/sop)
1 parent 8f0084c commit e87f9e7

4 files changed

Lines changed: 255 additions & 0 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ memory/L4_raw_sessions/*
106106
# Memory management
107107
!memory/memory_cleanup_sop.md
108108

109+
# Checklist/MapReduce
110+
!memory/checklist_helper.py
111+
!memory/checklist_sop.md
112+
109113
# Code Review Principles
110114
!memory/code_review_principles.md
111115

@@ -128,6 +132,7 @@ reflect/*
128132
!reflect/scheduler.py
129133
!reflect/agent_team_worker.py
130134
!reflect/goal_mode.py
135+
!reflect/checklist_master.py
131136

132137
# Universal: never track __pycache__ anywhere
133138
**/__pycache__/

memory/checklist_helper.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# checklist_helper.py — CL(folder) 一站式任务清单(支持 checklist/mapreduce 两种模式)
2+
import json, time, subprocess, socket, sys
3+
from pathlib import Path
4+
_R = Path(__file__).resolve().parent.parent
5+
_BBS, _MAIN = _R/"assets/agent_bbs.py", _R/"agentmain.py"
6+
_W_RE, _M_RE = _R/"reflect/agent_team_worker.py", _R/"reflect/checklist_master.py"
7+
_PK = {"stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL}
8+
if sys.platform == "win32": _PK["creationflags"] = 0x200
9+
10+
class CL:
11+
def __init__(self, folder, goal="", workers=0):
12+
"""
13+
workers=0: checklist模式,master自己逐个执行,不启动BBS
14+
workers>0: mapreduce模式,启动BBS+N个worker并行
15+
"""
16+
self.folder = Path(folder); self.folder.mkdir(parents=True, exist_ok=True)
17+
self.path = self.folder / "state.json"
18+
self.workers = workers
19+
if self.path.exists(): self._d = json.loads(self.path.read_text("utf-8"))
20+
else:
21+
self._d = {"closed": False, "goal": goal, "bbs": None, "tasks": []}
22+
self._save()
23+
if workers > 0:
24+
self._ensure_bbs()
25+
self.start_worker(workers)
26+
27+
@property
28+
def tasks(self): return self._d["tasks"]
29+
@property
30+
def closed(self): return self._d.get("closed", False)
31+
@property
32+
def has_open(self): return any(t["result"] is None for t in self.tasks)
33+
@property
34+
def bbs_url(self): return self._d["bbs"]["url"] if self._d["bbs"] else None
35+
@property
36+
def bbs_key(self): return self._d["bbs"]["key"] if self._d["bbs"] else None
37+
@property
38+
def mode(self): return "mapreduce" if self._d["bbs"] else "checklist"
39+
def _save(self): self.path.write_text(json.dumps(self._d, ensure_ascii=False, indent=1), "utf-8")
40+
41+
def _ensure_bbs(self):
42+
if self._d["bbs"]: return
43+
with socket.socket() as s: s.bind(('',0)); port = s.getsockname()[1]
44+
key = f"cl_{int(time.time())%1000}"
45+
(self.folder/"bbs").mkdir(exist_ok=True)
46+
subprocess.Popen(["python", str(_BBS), "--cwd", str(self.folder/"bbs"),
47+
"--port", str(port), "--key", key], **_PK)
48+
time.sleep(1)
49+
self._d["bbs"] = {"url": f"http://127.0.0.1:{port}", "key": key}
50+
self._save()
51+
52+
def add(self, texts):
53+
nid = max((t["id"] for t in self.tasks), default=0) + 1
54+
ids = []
55+
for t in texts:
56+
self.tasks.append({"id": nid, "text": t, "result": None, "ts": int(time.time())})
57+
ids.append(nid); nid += 1
58+
self._save(); return ids
59+
60+
def mark(self, tid, result):
61+
for t in self.tasks:
62+
if t["id"] == tid: t["result"] = result; t["ts"] = int(time.time()); break
63+
self._save()
64+
65+
def look(self):
66+
done = sum(1 for t in self.tasks if t["result"] is not None)
67+
lines = [f"[{done}/{len(self.tasks)}] mode={self.mode}"]
68+
for t in self.tasks:
69+
l = f'{"✓" if t["result"] else "○"} #{t["id"]} {t["text"][:60]}'
70+
if t["result"]: l += f' → {t["result"][:60]}'
71+
lines.append(l)
72+
return "\n".join(lines)
73+
74+
def close(self):
75+
assert not self.has_open, "has open tasks"
76+
self._d["closed"] = True; self._save()
77+
78+
def start_worker(self, n=None):
79+
n = n or self.workers or 1
80+
if n <= 0: return
81+
for i in range(n):
82+
subprocess.Popen(["python", str(_MAIN), "--reflect", str(_W_RE),
83+
"--base_url", self.bbs_url, "--board_key", self.bbs_key, "--name", f"w{i+1}"], **_PK)
84+
if i < n - 1: time.sleep(5)
85+
86+
def start_master(self):
87+
subprocess.Popen(["python", str(_MAIN), "--reflect", str(_M_RE),
88+
"--mr_folder", str(self.folder.resolve())], **_PK)

memory/checklist_sop.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Checklist SOP
2+
3+
## Booter(启动者/用户)
4+
5+
**Checklist 模式**(单人,master自己执行):
6+
```python
7+
from checklist_helper import CL
8+
cl = CL("cl_xxx", goal="<用户要求任务,尽量原样>")
9+
cl.start_master()
10+
```
11+
12+
**MapReduce 模式**(多人,master派发+worker执行):
13+
```python
14+
from checklist_helper import CL
15+
cl = CL("cl_xxx", goal="<用户要求任务,尽量原样>", workers=2)
16+
cl.start_master()
17+
```
18+
19+
goal 写法:只写「做什么 + 参考哪个SOP」,不写怎么做。Master 自己读 SOP 决定 plan。
20+
21+
## Master(reflect agent 使用)
22+
23+
```python
24+
from checklist_helper import CL
25+
cl = CL("cl_xxx") # 加载状态(BBS已在跑)
26+
cl.add(["任务1", "任务2"]) # 在你的笔记中记录TODO
27+
cl.look() # 查进度
28+
cl.mark(id, "摘要") # 验收
29+
cl.close() # 全部完成后关闭
30+
```
31+
32+
## Master plan示例
33+
34+
目标可分解为多个**不相干、可并行**的子任务 → add 子任务。
35+
B 要等 A 的结果 → 不要硬拆,串行做。
36+
37+
任务使用短句,派发时再补充信息。
38+
39+
1. 下载网盘 /game 下所有文件
40+
→ 先 webscan 拿文件列表,再每个文件一条任务
41+
`cl.add(["下载A.exe", "下载B.zip", "下载C.zip"])`
42+
43+
2. 从语法、风格、格式角度检查 a.pdf
44+
→ 三个维度天然独立
45+
`cl.add(["检查语法", "检查风格", "检查格式"])`
46+
47+
3. 查所有 VPS 中版本 < 22 的,升级到 24
48+
→ 第一轮:每台一条查版本任务
49+
`cl.add(["查 node03 版本", "查 node09 版本", "查 Dell 版本"])`
50+
→ reduce:master 筛出 < 22 的
51+
→ 第二轮:每台需升级的一条任务
52+
`cl.add(["升级 node03 到 24", "升级 Dell 到 24"])`
53+
54+
## Master 循环
55+
56+
```
57+
cl.look()
58+
├─ 有未完成任务 → 去 BBS 派发(mapreduce模式)/ 挑最简单的**不派发**自己干 / 自己干(无worker checklist模式)
59+
└─ 全部完成
60+
├─ 用户最终目标已达成 → close()
61+
└─ 最终目标未达成 → plan 下一步
62+
├─ 可解耦 → add() 新一批任务
63+
├─ 需串行前置 → 自己做一步,再回 look
64+
└─ 基本搞定 → 自己整合结果,交付最终报告
65+
```
66+
67+
master会被持续唤醒直到其显式成功调用close()。
68+
69+
## 派发任务(有workers模式下)
70+
71+
worker无法看到add的任务,只能看到BBS!
72+
每条任务 prompt 须**自包含**——worker 没有 master 的上下文。
73+
每次最多只派发3个任务,不要一次性把所有任务贴到bbs上。
74+
worker足够聪明,只允许写目标和需要的信息,不要干预
75+
**master不允许执行已经派发出去的任务,会导致重复执行!**
76+
77+
写 prompt 要点:
78+
1. **背景**:worker 需要的信息直接给(路径、数据、约定),不要假设 worker 知道
79+
2. **交付物**:明确产出什么、格式、写到哪里
80+
3. **不限手段**:说要什么结果,别规定怎么做
81+
4. **不干预 BBS 行为**:禁止教 worker 如何抢单/回帖/报告,那是 worker 自己的机制
82+
83+
交付规范(写进任务 prompt):
84+
- 交付结果和报告信息必须分开。交付 = 纯成品;报告 = 过程/问题/备注
85+
- 交付文件禁止出现说明性废话
86+
- 长结果写文件,短结果直接回帖
87+
88+
## 验收
89+
90+
Master 收到 worker 回帖或自己完成子任务后:
91+
- 检查结果,语义判断 pass/fail → `cl.mark(id, "结果摘要")`
92+
- 交付物含过程废话 → 要求重写交付物
93+
- 失败 → 可重发、换 prompt、或自己补
94+
95+
## 注意
96+
97+
- 若子任务需要 web 工具,提醒并行 worker 新建 tab 并使用自己的 tab

reflect/checklist_master.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import json, time, random
2+
from pathlib import Path
3+
from urllib import request
4+
5+
INTERVAL = 60
6+
ONCE = False
7+
_folder = None
8+
_last_post_id = -1
9+
10+
def init(a):
11+
global _folder
12+
_folder = Path(a.get('mr_folder', ''))
13+
14+
def _load():
15+
p = _folder / "state.json"
16+
if not p.exists(): return None
17+
return json.loads(p.read_text("utf-8"))
18+
19+
def _poll_bbs(data):
20+
global _last_post_id
21+
bbs = data.get("bbs")
22+
if not bbs: return []
23+
url, key = bbs.get("url", ""), bbs.get("key", "")
24+
if not url: return []
25+
try:
26+
req = request.Request(f"{url}/posts?limit=20&key={key}")
27+
posts = json.loads(request.urlopen(req, timeout=10).read())
28+
if not posts: return []
29+
new = [p for p in posts if p['id'] > _last_post_id]
30+
_last_post_id = max(p['id'] for p in posts)
31+
return new
32+
except Exception:
33+
return []
34+
35+
def check():
36+
if not _folder: return '/exit'
37+
data = _load()
38+
if not data or data.get("closed"): return '/exit'
39+
bbs = data.get("bbs")
40+
if not bbs: return _prompt(data, [])
41+
# mapreduce: 轮询BBS
42+
new_posts = _poll_bbs(data)
43+
tasks = data.get("tasks", [])
44+
has_open = any(t["result"] is None for t in tasks)
45+
if new_posts and has_open: return _prompt(data, new_posts)
46+
if not has_open and (not tasks or random.random() < 0.2): return _prompt(data, new_posts)
47+
return None
48+
49+
def _prompt(data, new_posts):
50+
bbs = data.get("bbs")
51+
goal = data.get("goal", "")
52+
mode = "mapreduce" if bbs else "checklist"
53+
if new_posts:
54+
trigger = "有新回帖,去BBS查看并验收"
55+
elif any(t["result"] is None for t in data.get("tasks", [])):
56+
trigger = "有未完成任务,继续执行" if not bbs else "有未完成任务,派发或自己执行"
57+
else:
58+
trigger = "无未完成任务,该plan下一步了"
59+
lines = [f"你是 Checklist Master({mode}模式)。阅读 checklist_sop.md 按 Master 循环行事。"]
60+
if bbs:
61+
lines.append(f"BBS API文档(requests): GET {bbs['url']}/readme?key={bbs['key']}")
62+
lines.append(f"目标: {goal}")
63+
lines.append(f"唤醒原因: {trigger}")
64+
lines.append(f'用 checklist_helper 的 CL("{_folder}") 管理状态(look/add/mark/close)。按决策树行动。')
65+
return "\n".join(lines)

0 commit comments

Comments
 (0)