pi-multi-viewers 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -2
- package/README.md +13 -6
- package/docs/design.md +60 -2
- package/docs/test-methodology.md +23 -0
- package/fake_agent.py +16 -5
- package/human_viewer.py +112 -16
- package/meeting_core.py +28 -9
- package/meeting_engine.py +53 -34
- package/meeting_fs.py +20 -0
- package/meeting_loop.py +1 -1
- package/observability.py +397 -0
- package/package.json +1 -1
- package/prompts/multi-viewers.md +5 -4
- package/spec_gen.py +518 -0
- package/start_discussion.py +47 -828
package/AGENTS.md
CHANGED
|
@@ -18,7 +18,9 @@ meeting_fs.py git/文件层
|
|
|
18
18
|
meeting_engine.py 【唯一状态机】+ 协议信号 + responder 注入
|
|
19
19
|
meeting_loop.py Pi 薄壳:首唤生成 fork 源 + `--session` 打开 → 存 sid → `--session-id` 续接
|
|
20
20
|
fake_agent.py 测试薄壳:responder = 随机决策
|
|
21
|
-
start_discussion.py
|
|
21
|
+
start_discussion.py 组合层:CLI 分发 + 环境创建/启动/清理(_resolve_spec / setup_environment)
|
|
22
|
+
spec_gen.py spec 生成层(question/骨架/viewers 校验与快照/agent 定义 + pi 环境探测)
|
|
23
|
+
observability.py 观测层(check_status / --report / --wait / loop 存活检测)
|
|
22
24
|
human_viewer.py 【human 通道】只读展示(增量/--follow/游标)
|
|
23
25
|
human_sayer.py 【human 通道】插话命令(单次/stdin/交互 -i)
|
|
24
26
|
scripts/mv.sh wrapper(prepare/start/status/wait/cleanup/view/say)
|
|
@@ -174,7 +176,7 @@ loop、状态从 git 共享事实推导、单一事实源 = protocol.json、无
|
|
|
174
176
|
extension × 1(multi-viewers-say 插话:零 LLM,直接 spawn human_sayer.py;
|
|
175
177
|
目录发现 = `<cwd>/mv-<sessionId>-*` 最新,兜底 `mv-*`(排除
|
|
176
178
|
`mv-spec-*`)并警告)
|
|
177
|
-
+ wrapper。**npm 已发布 0.1
|
|
179
|
+
+ wrapper。**npm 已发布 0.2.1(2026-09-11)**。
|
|
178
180
|
- **开发机安装(两步,缺一不可;2026-09-10 实测)**:
|
|
179
181
|
① `pi install /root/pi-multi-viewers`——**注册包**(写
|
|
180
182
|
`~/.pi/agent/settings.json` 的 `packages` 数组);pi 不是"扫 node_modules
|
package/README.md
CHANGED
|
@@ -89,6 +89,8 @@ scripts/mv.sh --start <spec目录> # 启动(自动挂载主 sessi
|
|
|
89
89
|
|
|
90
90
|
# 观看:--start 会输出可直接执行的 !! 流式观看命令(复制执行)
|
|
91
91
|
scripts/mv.sh --view <dir> # 或一次性增量查看(主 pi 记录 HEAD 作下轮 --since)
|
|
92
|
+
# --follow 会打印【状态】(meeting/all-freezing/round-robin/concluded)
|
|
93
|
+
# 与【进度】(meeting 消耗/上限 | freezing 集合 | rr → 下一位)
|
|
92
94
|
|
|
93
95
|
# 插话 / 状态 / 收尾
|
|
94
96
|
scripts/mv.sh --say <dir> "<文本>" # 插话(命令行形态;pi 内用 /multi-viewers-say)
|
|
@@ -132,14 +134,19 @@ frontmatter 字段、写文件路径、独立参与者纪律。
|
|
|
132
134
|
## 架构
|
|
133
135
|
|
|
134
136
|
```
|
|
135
|
-
meeting_core.py
|
|
136
|
-
meeting_fs.py
|
|
137
|
-
meeting_engine.py
|
|
138
|
-
meeting_loop.py
|
|
139
|
-
start_discussion.py
|
|
140
|
-
|
|
137
|
+
meeting_core.py 纯判定(冻结级联/RR/聚合 + 状态机词汇常量)
|
|
138
|
+
meeting_fs.py git 层 + fork 源生成/裁剪 + 协议与产物常量
|
|
139
|
+
meeting_engine.py 唯一状态机(六分支)
|
|
140
|
+
meeting_loop.py Pi 薄壳(fork 首唤 + --session-id 续接 + 视角注入)
|
|
141
|
+
start_discussion.py 组合层:CLI 分发 + 环境创建/启动/清理
|
|
142
|
+
spec_gen.py spec 生成(question/骨架/viewers 快照/agent 定义 + pi 环境探测)
|
|
143
|
+
observability.py 观测(check_status / --report / --wait / loop 存活)
|
|
144
|
+
human_viewer/sayer human 插话通道
|
|
141
145
|
```
|
|
142
146
|
|
|
147
|
+
依赖方向单向:`core ← fs ← engine ← loop`;`start_discussion` 组合
|
|
148
|
+
`spec_gen` / `observability`(两者只依赖底层,互不依赖、不反向依赖主文件)。
|
|
149
|
+
|
|
143
150
|
## 开发
|
|
144
151
|
|
|
145
152
|
```bash
|
package/docs/design.md
CHANGED
|
@@ -100,7 +100,18 @@ compaction 的 `firstKeptEntryId` 起 + 其后的条目"——窗口内含 compa
|
|
|
100
100
|
| `status-<agent>.json` | loop | `{"sessionID": ...}` | 流程(崩溃恢复) | 是(恢复用) | O(1) |
|
|
101
101
|
| `pi-sessions/fork-src-*.jsonl` | pi | 文档化 session schema | fork 构建 + `--report` | 否(报告用) | O(MB) 全量 → **禁轮询** |
|
|
102
102
|
| `result.md`(固定位) | resultWriter loop | 结论文档 | 人 | 是(收尾判据) | — |
|
|
103
|
-
| `--report`(视图) |
|
|
103
|
+
| `--report`(视图) | observability | 文本行 | 人(**三个出口**,见下) | **否**(不得升级为验收 gate) | 冷路径一次性 |
|
|
104
|
+
|
|
105
|
+
**报告的三个出口**(同一 `build_report`,同一份内容):
|
|
106
|
+
1. **`--follow` 结束**——viewer 在 done 分支自动附报告(`human_viewer._print_report`)。
|
|
107
|
+
这是**用户通道自带**:用户执行 `!!` 命令就在结束时直接看到,**零 LLM 参与**
|
|
108
|
+
(此前只靠 prompt 要求主 pi"记得转述"——那是 LLM 依赖,会漏;机制化后
|
|
109
|
+
用户必然看到)。
|
|
110
|
+
2. **`--cleanup`**——删目录前最后一次可读(结果与 1 重复出现是刻意的:
|
|
111
|
+
不同时点各看一次,且清理后现场已不存在)。
|
|
112
|
+
3. **`--report`**——独立入口(中途查看 / 脚本消费)。
|
|
113
|
+
`--view --since`(主 pi 增量轮询通道)**不附报告**——它面向 LLM,
|
|
114
|
+
输出进 context,报告对模型无用且占 token。
|
|
104
115
|
|
|
105
116
|
### 本轮边界(`mv.analysis-start`)
|
|
106
117
|
|
|
@@ -112,6 +123,15 @@ BOUNDARY_TYPE`)——**显式登记"历史(fork 携带)/ 本轮"的分界*
|
|
|
112
123
|
推断都会漂移(切换叙事改措辞、时钟精度)。2026-09-11 实测:不带边界时报告
|
|
113
124
|
把 717 条 fork 历史算成"本轮 367 次响应 / input 1.2M / cacheRead 136.6M"。
|
|
114
125
|
|
|
126
|
+
**viewer 进度行(三样观测面)**:`--follow` 每轮在【状态】之外打印
|
|
127
|
+
【进度】行——`meeting <消耗>/<上限> · … | freezing <已冻结>/<总数>(名单)
|
|
128
|
+
| rr → <下一位>`。**状态名一律用协议术语**(`meeting`/`freezing`/`rr`;
|
|
129
|
+
译成中文会引入第二套命名,"冻结"到底指 freezing 还是 all-freezing 说不清)。
|
|
130
|
+
判定与 `--report` 共用 core/engine 单一实现(`meeting_speak_count` /
|
|
131
|
+
`frozen_agents` / `rr_next_speaker`),且共用调用方已读的 `msgs`——
|
|
132
|
+
**零新增 bare 读取**(实测改动前后同为 6 次 run 调用 + 1 次 cat-file 批读;
|
|
133
|
+
`rr` 位置仅在 RR 阶段按需调权威实现)。进度行**只在变化时打印**(避免刷屏)。
|
|
134
|
+
|
|
115
135
|
**报告的打印位置**:`--report`(手动,任意时刻)+ `--cleanup` 前(自动,
|
|
116
136
|
删目录前最后一次可读——目录删后 `--report` 不可用)。cleanup 层对报告
|
|
117
137
|
fail-open(报告失败不阻断清理,且**打印**失败原因不静默)。
|
|
@@ -224,7 +244,45 @@ commit 是溯源记录、本节是长期引用点——不并存两份权威值
|
|
|
224
244
|
`mv-spec-*`)并**警告降级**
|
|
225
245
|
(宁可提示也不静默插错分析)。观看仍用 `!!` 流式(命令 API 无原生流式
|
|
226
246
|
通道,bash 流式是平台原生能力)。
|
|
227
|
-
10. **
|
|
247
|
+
10. **question.md 的主题行措辞 = `# 分析主题:`(唯一)**:生成端
|
|
248
|
+
(`gen_question` / `gen_spec_skeleton`)、模板(`spec-readme`)、prompt、
|
|
249
|
+
消费端(`setup_environment` 提取 `protocol.topic`)四处同一措辞;
|
|
250
|
+
消费端**只认它**,旧措辞 spec → fail-fast(明确报错,不静默退化)。
|
|
251
|
+
曾出现双轨(生成产 `# 讨论主题:`、消费端写兼容循环兜两种)——那
|
|
252
|
+
正是"补丁掩盖设计缺陷"的形态(不改生产、只兜消费端)。
|
|
253
|
+
11. **stall 接管 = 心跳式软仲裁(非互斥)**:非 rw 在无进展超时时接管收尾,
|
|
254
|
+
先 pull 重检共享事实(concluded / result.md)→ 写接管声明 commit
|
|
255
|
+
(**只为推进 HEAD**,使对方 `_stall_elapsed` 归零而退出该分支)→
|
|
256
|
+
finalize。毫秒级同轮窗口存在(双方都可能 finalize),但**不是死锁**:
|
|
257
|
+
靠 push 容错 + 下轮 concluded 退出兜底,产物始终唯一。声明只降低并发
|
|
258
|
+
概率,**不构成互斥保证**(注释勿写"天然唯一")。实测(2026-09-11,
|
|
259
|
+
381 测试中的 `TestStallTakeover`):a 进入接管 0.11s 内完成声明→收尾→
|
|
260
|
+
concluded,b 晚 1.4s 只见 concluded 即退出——正常时序下软仲裁生效。
|
|
261
|
+
12. **`result.md` 的文件名与有效性阈值 = `meeting_fs.RESULT_MD` /
|
|
262
|
+
`RESULT_MD_MIN_BYTES`**:产品级核心产物,此前 8 处字面量分散在
|
|
263
|
+
engine/loop/viewer/start_discussion/fake_agent(`repo.git` 早已收归
|
|
264
|
+
fs 层,产物名却没有家)。文件叫什么、多大算有效——同一概念的两个
|
|
265
|
+
数字住在一起。
|
|
266
|
+
13. **分层 = core ← fs ← engine ← loop,`start_discussion` 是组合层**
|
|
267
|
+
(S2 拆分后):三项职责按"谁消费"拆到两个同层模块——
|
|
268
|
+
`spec_gen.py`(分析前的静态产物生成 + 为其服务的 pi 环境探测)、
|
|
269
|
+
`observability.py`(运行期只读观测);主文件保留 CLI 分发、环境创建、
|
|
270
|
+
启动、清理与编排。**判据是职责而非行数**:spec 生成与观测各自 350–520
|
|
271
|
+
行、概念内聚,再拆会制造碎片(import 网变复杂而收益递减)。
|
|
272
|
+
两个子模块只依赖底层(core/fs/engine),互不依赖、不反向依赖主文件;
|
|
273
|
+
主文件 re-export 子模块公开符号,保证 `from start_discussion import X`
|
|
274
|
+
的既有调用方(tests/wrapper)零改动。
|
|
275
|
+
**拆分暴露的纪律**:`mock.patch` 必须打在**符号定义处**(拆前
|
|
276
|
+
`start_discussion.check_status` 与定义处同址,拆后 mock re-export
|
|
277
|
+
不生效——本轮 4 处测试因此假绿/失败,已改到 `spec_gen` /
|
|
278
|
+
`observability`)。
|
|
279
|
+
14. **报告附在 `--follow` 输出末尾(机制化,不依赖 LLM)**:`--follow` 是
|
|
280
|
+
用户直接执行的通道(`!!`),done 时自动打印报告——用户零操作看到运行
|
|
281
|
+
事实。**为什么不能只靠 prompt**:让主 pi"记得跑 `--report` 并转述"是
|
|
282
|
+
流程依赖 LLM(会漏、不可验收),正是本项目一贯要消除的形态;报告既然
|
|
283
|
+
是给用户的,就该长在用户直接看的通道上。`--view --since` 不附(主 pi
|
|
284
|
+
通道,进 context 且对模型无用)。
|
|
285
|
+
15. **git 守卫范围 = 从讨论 workdir 发起的操作**(`GIT_CEILING_DIRECTORIES`
|
|
228
286
|
注入于 spawn);主项目仓库不在守卫范围(agent 的 cwd 就是主项目,其
|
|
229
287
|
约束归指令层 + 主项目 `.gitignore`)。要拦主仓库需换机制类(沙箱/钩子),
|
|
230
288
|
经评估收益不支撑扩面。
|
package/docs/test-methodology.md
CHANGED
|
@@ -256,3 +256,26 @@ python 时被"顺手改写"为含斜杠则不拼——**移植不是重写**:
|
|
|
256
256
|
3. 删除函数区间用"下一个函数名"作终点前,先 `grep` 确认区间内没有别的定义;
|
|
257
257
|
4. 提交前跑**真实调用路径**:`python3 -c "import <mod>"`(抓未定义/未导入名)
|
|
258
258
|
+ 相关测试。两次失误都是真实路径抓到的——静态阅读与"看起来对"都不够。
|
|
259
|
+
|
|
260
|
+
### 19. 验证分层:测试挂在"代码改动"上,不挂在"发版"上(2026-09-11 发版冗余)
|
|
261
|
+
|
|
262
|
+
**背景**:发版时顺手跑全量测试(382 测试 / 5 分钟)看起来"更稳妥",实为冗余
|
|
263
|
+
——发版动作本身只改 `package.json` 的版本号与文档文案,**零代码改动**;而区间
|
|
264
|
+
内的代码改动在其**实现提交时**已经跑过全量测试。
|
|
265
|
+
|
|
266
|
+
**判据**:「这段区间内有没有**未被验证**的代码改动?」——有则跑,没有则不跑。
|
|
267
|
+
不是「是否在发版」「是否要推送」「是否重要」。
|
|
268
|
+
|
|
269
|
+
**分层**:
|
|
270
|
+
|
|
271
|
+
| 时点 | 验证内容 |
|
|
272
|
+
|---|---|
|
|
273
|
+
| 实现提交 | **全量测试**(代码改了;这是唯一的测试时机) |
|
|
274
|
+
| 发版 | **打包验证**:`npm pack --dry-run` 核清单 + 真实安装后跑 CLI 冒烟 |
|
|
275
|
+
| 推送 | 无需测试(提交时已验)——只核 `git status` 干净 + 本地/远程一致 |
|
|
276
|
+
|
|
277
|
+
**例外**(唯一合理场景):发版区间累积了多个提交、且距上次全量测试较久
|
|
278
|
+
(如跨天),此时补跑一次合理——因为"区间内改动已被验证"这个前提可能不成立。
|
|
279
|
+
|
|
280
|
+
**成本视角**:冗余测试不只是耗时——它稀释了"全绿"的信号价值(每次发版都全绿
|
|
281
|
+
时,人不再区分"这次真的验了代码"与"这次只 bump 了版本号")。
|
package/fake_agent.py
CHANGED
|
@@ -21,6 +21,8 @@ import time
|
|
|
21
21
|
|
|
22
22
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
23
23
|
|
|
24
|
+
import meeting_core
|
|
25
|
+
import meeting_fs
|
|
24
26
|
from meeting_fs import next_msg_id, write_message
|
|
25
27
|
from meeting_engine import agent_loop
|
|
26
28
|
|
|
@@ -65,7 +67,7 @@ def make_responder(min_sleep, max_sleep, crash_rate):
|
|
|
65
67
|
# --- 内容决定(LLM 的判断)---
|
|
66
68
|
if finalizing:
|
|
67
69
|
# 收尾指令:resultWriter 写 result.md(双面约束:此时才允许写)
|
|
68
|
-
result_path = os.path.join(workdir,
|
|
70
|
+
result_path = os.path.join(workdir, meeting_fs.RESULT_MD)
|
|
69
71
|
# 内容 >50 字节(_result_md_valid 阈值,审核#3)——否则恒走
|
|
70
72
|
# loop 兜底代写,正常收尾路径零验证(fake 与真实 responder 语义不一致)
|
|
71
73
|
# 文案三分支(review4 L12):stall 不误标"配额耗尽"
|
|
@@ -81,13 +83,14 @@ def make_responder(min_sleep, max_sleep, crash_rate):
|
|
|
81
83
|
return True
|
|
82
84
|
if retry:
|
|
83
85
|
# 被重试(上次没产出):无静默铁律,强制表态
|
|
84
|
-
decision =
|
|
86
|
+
decision = meeting_core.T_FREEZING if not rr_turn else meeting_core.T_PASS
|
|
85
87
|
elif rr_turn:
|
|
86
88
|
# RR 阶段:单向流,只写 pass(无异议回退,留待以后)
|
|
87
|
-
decision =
|
|
89
|
+
decision = meeting_core.T_PASS
|
|
88
90
|
else:
|
|
89
91
|
# meeting 阶段:有内容 → message;无话可说 → freezing
|
|
90
|
-
decision =
|
|
92
|
+
decision = meeting_core.T_MESSAGE if random.random() < 0.65 \
|
|
93
|
+
else meeting_core.T_FREEZING
|
|
91
94
|
|
|
92
95
|
time.sleep(random.uniform(min_sleep, max_sleep))
|
|
93
96
|
if random.random() < crash_rate:
|
|
@@ -116,5 +119,13 @@ if __name__ == "__main__":
|
|
|
116
119
|
workdir, agent = sys.argv[1], sys.argv[2]
|
|
117
120
|
responder = make_responder(float(sys.argv[3]), float(sys.argv[4]),
|
|
118
121
|
float(sys.argv[5]))
|
|
122
|
+
# stall 超时:与生产(meeting_loop.__main__)同款——protocol 优先
|
|
123
|
+
# (装置对齐生产:此前 fake_agent 恒用默认 600s,stall 路径在测试中
|
|
124
|
+
# 永不可达——P2 的接管分支零覆盖正是这个原因)
|
|
125
|
+
stall_timeout = meeting_fs.DEFAULT_STALL_TIMEOUT
|
|
126
|
+
proto = meeting_fs.read_protocol(meeting_fs.bare_of_workdir(workdir))
|
|
127
|
+
if proto.get("stallTimeoutSeconds"):
|
|
128
|
+
stall_timeout = proto["stallTimeoutSeconds"]
|
|
119
129
|
agent_loop(workdir, agent, responder,
|
|
120
|
-
max_meeting=int(sys.argv[6]), max_rr=int(sys.argv[7])
|
|
130
|
+
max_meeting=int(sys.argv[6]), max_rr=int(sys.argv[7]),
|
|
131
|
+
stall_timeout=stall_timeout)
|
package/human_viewer.py
CHANGED
|
@@ -28,10 +28,17 @@ import os
|
|
|
28
28
|
import sys
|
|
29
29
|
import time
|
|
30
30
|
|
|
31
|
+
import meeting_core
|
|
31
32
|
import meeting_fs
|
|
32
33
|
from meeting_fs import (run_git, git_show, git_head, is_message_file,
|
|
33
34
|
parse_log_nameonly, extract_body, parse_frontmatter)
|
|
34
|
-
from
|
|
35
|
+
from meeting_core import (
|
|
36
|
+
aggregate_mode as core_aggregate_mode,
|
|
37
|
+
frozen_agents,
|
|
38
|
+
meeting_speak_count,
|
|
39
|
+
)
|
|
40
|
+
from meeting_engine import (each_agent_last, each_agent_messages,
|
|
41
|
+
rr_next_speaker)
|
|
35
42
|
|
|
36
43
|
|
|
37
44
|
def participants_from_bare(bare):
|
|
@@ -94,23 +101,32 @@ def is_finished(bare, agents, mode=None):
|
|
|
94
101
|
为什么落 viewer:它是唯一增量实现的持有者,也是观察端判据的家。
|
|
95
102
|
"""
|
|
96
103
|
if mode is None:
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
# 经 engine.each_agent_last(它把最后一条规范化成 {type, mode, next}
|
|
105
|
+
# ——缺字段补 None);直接传原始 frontmatter 会在 core 里 KeyError
|
|
106
|
+
# (core 用 v["mode"] 按契约取值,不猜缺字段)
|
|
107
|
+
mode = core_aggregate_mode(each_agent_last(bare, agents))
|
|
108
|
+
if mode != meeting_core.M_CONCLUDED:
|
|
99
109
|
return False
|
|
100
|
-
content = git_show(bare, "HEAD",
|
|
101
|
-
return bool(content) and len(content) >
|
|
110
|
+
content = git_show(bare, "HEAD", meeting_fs.RESULT_MD)
|
|
111
|
+
return bool(content) and len(content) > meeting_fs.RESULT_MD_MIN_BYTES
|
|
102
112
|
|
|
103
113
|
|
|
104
|
-
def incremental(bare, agents, since):
|
|
114
|
+
def incremental(bare, agents, since, max_meeting=None):
|
|
105
115
|
"""单次增量读取。
|
|
106
116
|
|
|
107
|
-
返回: (mode, lines: list[str], head, done)
|
|
117
|
+
返回: (mode, lines: list[str], head, done, progress)
|
|
108
118
|
- mode: 当前聚合 mode(meeting/all-freezing/round-robin/concluded)
|
|
109
119
|
- lines: 新消息展示行(旧→新)
|
|
110
120
|
- head: 当前 HEAD
|
|
111
|
-
- done: 分析是否已收尾(
|
|
121
|
+
- done: 分析是否已收尾(concluded 且 result.md 有效)
|
|
122
|
+
- progress: 进度行文本(三样观测面,见 progress_text)
|
|
123
|
+
|
|
124
|
+
**单次读取**:一次 each_agent_messages → 派生 mode 与全部三样观测面
|
|
125
|
+
(mode 判定、freezing 集合、meeting 配额),不重复读 bare。
|
|
112
126
|
"""
|
|
113
|
-
|
|
127
|
+
msgs = each_agent_messages(bare, agents)
|
|
128
|
+
lasts = {a: (msgs[a][-1] if msgs[a] else None) for a in agents}
|
|
129
|
+
mode = core_aggregate_mode(lasts)
|
|
114
130
|
lines = []
|
|
115
131
|
for commit, path in new_messages(bare, since):
|
|
116
132
|
content = git_show(bare, commit, path)
|
|
@@ -120,7 +136,43 @@ def incremental(bare, agents, since):
|
|
|
120
136
|
if s:
|
|
121
137
|
lines.append(s)
|
|
122
138
|
head = git_head(bare)
|
|
123
|
-
return mode, lines, head, is_finished(bare, agents, mode)
|
|
139
|
+
return (mode, lines, head, is_finished(bare, agents, mode),
|
|
140
|
+
progress_text(bare, agents, msgs, lasts, mode, max_meeting))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def progress_text(bare, agents, msgs, lasts, mode, max_meeting=None):
|
|
144
|
+
"""进度行:三样观测面(**状态名一律用协议术语**,不翻译)。
|
|
145
|
+
|
|
146
|
+
形态:`meeting 7/10 · 6/10 | freezing 2/3(性能、简单)| rr → 简单`
|
|
147
|
+
|
|
148
|
+
- `meeting`:配额消耗/上限(`meeting_core.meeting_speak_count` 口径
|
|
149
|
+
——"mode:meeting 且 type:message"的发言轮;上限来自 protocol,
|
|
150
|
+
调用方传入;未传则只显示消耗数)。
|
|
151
|
+
- `freezing`:已冻结数/总数(`meeting_core.frozen_agents`)。
|
|
152
|
+
- `rr`:RR 阶段轮到谁(**权威实现** `rr_next_speaker`;仅
|
|
153
|
+
round-robin 阶段出现)。
|
|
154
|
+
|
|
155
|
+
**为什么用协议术语而非中文**(用户 2026-09-11):状态名是协议的
|
|
156
|
+
一等概念(`meeting`/`freezing`/`round-robin`),翻译成中文会引入
|
|
157
|
+
第二套命名("冻结"到底指 freezing 还是 all-freezing?)——协议名
|
|
158
|
+
在代码、消息 frontmatter、`--report`、viewer 里只有一个说法。
|
|
159
|
+
|
|
160
|
+
判定全部复用 core/engine 的单一实现(与 `--report` 同一份),
|
|
161
|
+
且共用调用方已读的 msgs——**零新增 bare 读取**;仅 rr 位置在
|
|
162
|
+
RR 阶段调权威实现(需要读 HEAD 的文件列表,属必要成本)。
|
|
163
|
+
"""
|
|
164
|
+
types = {a: (lasts[a].get("type") if lasts[a] else None) for a in agents}
|
|
165
|
+
parts = []
|
|
166
|
+
counts = " · ".join(f"{meeting_speak_count(msgs, a)}"
|
|
167
|
+
+ (f"/{max_meeting}" if max_meeting else "")
|
|
168
|
+
for a in agents)
|
|
169
|
+
parts.append(f"meeting {counts}")
|
|
170
|
+
frozen = frozen_agents(agents, types)
|
|
171
|
+
parts.append(f"freezing {len(frozen)}/{len(agents)}"
|
|
172
|
+
+ (f"({'、'.join(frozen)})" if frozen else ""))
|
|
173
|
+
if mode == meeting_core.M_ROUND_ROBIN:
|
|
174
|
+
parts.append(f"rr → {rr_next_speaker(bare, agents) or '(未定)'}")
|
|
175
|
+
return " | ".join(parts)
|
|
124
176
|
|
|
125
177
|
|
|
126
178
|
def _cursor_path(base):
|
|
@@ -150,15 +202,27 @@ def _write_cursor(base, ref):
|
|
|
150
202
|
OBSERVER_POLL_INTERVAL = 2.0
|
|
151
203
|
|
|
152
204
|
|
|
153
|
-
def follow(base, bare, agents,
|
|
154
|
-
|
|
205
|
+
def follow(base, bare, agents, max_meeting=None,
|
|
206
|
+
poll_interval=OBSERVER_POLL_INTERVAL):
|
|
207
|
+
"""--follow:循环展示(tail -f 式)直到分析结束。
|
|
208
|
+
|
|
209
|
+
状态名用协议术语(`【状态】meeting` / `all-freezing` / `round-robin`
|
|
210
|
+
/ `concluded`);进度行见 progress_text。
|
|
211
|
+
"""
|
|
155
212
|
since = _read_cursor(base)
|
|
156
213
|
last_mode = None
|
|
214
|
+
last_progress = None
|
|
157
215
|
while True:
|
|
158
|
-
mode, lines, head, done = incremental(
|
|
216
|
+
mode, lines, head, done, progress = incremental(
|
|
217
|
+
bare, agents, since, max_meeting)
|
|
159
218
|
if mode != last_mode:
|
|
160
219
|
print(f"【状态】{mode}", flush=True)
|
|
161
220
|
last_mode = mode
|
|
221
|
+
# 进度行只在**变化时**打印(每轮都打会刷屏;冻结/配额/RR 位
|
|
222
|
+
# 置在两次消息之间本就不变)
|
|
223
|
+
if progress != last_progress:
|
|
224
|
+
print(f"【进度】{progress}", flush=True)
|
|
225
|
+
last_progress = progress
|
|
162
226
|
for s in lines:
|
|
163
227
|
print(s, flush=True)
|
|
164
228
|
if head != since:
|
|
@@ -167,10 +231,37 @@ def follow(base, bare, agents, poll_interval=OBSERVER_POLL_INTERVAL):
|
|
|
167
231
|
if done:
|
|
168
232
|
print(f"【分析已结束】result.md: {result_path(base)}",
|
|
169
233
|
flush=True)
|
|
234
|
+
_print_report(base)
|
|
170
235
|
return
|
|
171
236
|
time.sleep(poll_interval)
|
|
172
237
|
|
|
173
238
|
|
|
239
|
+
def _print_report(base):
|
|
240
|
+
"""分析结束时打印观测报告(**用户通道自带**,不依赖任何 LLM 动作)。
|
|
241
|
+
|
|
242
|
+
为什么在这里:`--follow` 是用户直接执行的通道(`!!` 命令),结束时
|
|
243
|
+
自动附报告 = 用户零操作看到运行事实(提交/墙钟/配额/进程/LLM 用量),
|
|
244
|
+
而不是指望主 pi 记得去跑 `--report` 再转述(LLM 依赖,可能漏)。
|
|
245
|
+
`--report` 独立入口与 `--cleanup` 的打印保持不变(不同场景各看一次)。
|
|
246
|
+
|
|
247
|
+
**延迟 import observability**:该模块顶层 import 本模块
|
|
248
|
+
(wait_for_completion 用 incremental),顶层反向 import 会成环。本函数
|
|
249
|
+
只在 done 分支执行一次(冷路径),函数内 import 是标准解法。
|
|
250
|
+
|
|
251
|
+
fail-open:报告是附加信息,生成失败绝不阻断观看退出(契约同
|
|
252
|
+
observability.build_report——任何一段读不出显示 n/a)。
|
|
253
|
+
"""
|
|
254
|
+
try:
|
|
255
|
+
from observability import build_report
|
|
256
|
+
print("【分析报告】", flush=True)
|
|
257
|
+
for line in build_report(base):
|
|
258
|
+
print(line, flush=True)
|
|
259
|
+
except Exception as e: # noqa: BLE001
|
|
260
|
+
# 宽捕获是刻意的:报告在观看主循环的退出路径上,任何异常
|
|
261
|
+
# (含未预期)都不该让用户失去"分析已结束"这个关键信息
|
|
262
|
+
print(f"【分析报告】生成失败(不影响观看):{e}", flush=True)
|
|
263
|
+
|
|
264
|
+
|
|
174
265
|
def main():
|
|
175
266
|
parser = argparse.ArgumentParser(description="human 分析展示(只读)")
|
|
176
267
|
parser.add_argument("base", help="分析目录(含 repo.git)")
|
|
@@ -184,17 +275,22 @@ def main():
|
|
|
184
275
|
print(f"错误: 分析不存在: {base}", file=sys.stderr)
|
|
185
276
|
return 1
|
|
186
277
|
|
|
187
|
-
|
|
278
|
+
proto = meeting_fs.read_protocol(bare) # 一次读取:参与者 + 配额上限
|
|
279
|
+
agents = proto.get("participants") or None
|
|
188
280
|
if not agents:
|
|
189
281
|
print(f"错误: 无法读取 protocol.json(分析未初始化?): {base}",
|
|
190
282
|
file=sys.stderr)
|
|
191
283
|
return 1
|
|
284
|
+
max_meeting = proto.get("maxMeetingRounds")
|
|
192
285
|
|
|
193
286
|
sys.stdout.reconfigure(line_buffering=True)
|
|
194
287
|
if args.follow:
|
|
195
|
-
follow(base, bare, agents)
|
|
288
|
+
follow(base, bare, agents, max_meeting=max_meeting)
|
|
196
289
|
else:
|
|
197
|
-
mode, lines, _, done = incremental(
|
|
290
|
+
mode, lines, _, done, progress = incremental(
|
|
291
|
+
bare, agents, args.since, max_meeting)
|
|
292
|
+
print(f"【状态】{mode}", flush=True)
|
|
293
|
+
print(f"【进度】{progress}", flush=True)
|
|
198
294
|
print(f"【状态】{mode}", flush=True)
|
|
199
295
|
for s in lines:
|
|
200
296
|
print(s, flush=True)
|
package/meeting_core.py
CHANGED
|
@@ -16,6 +16,25 @@
|
|
|
16
16
|
MEETING_TYPES = {"message", "freezing", "all-freezing", "pass", "concluded"}
|
|
17
17
|
"""meeting 模式合法 type 集合(校验器白名单)"""
|
|
18
18
|
|
|
19
|
+
# ---------------------------------------------------------------
|
|
20
|
+
# 状态机词汇(**字面量的家**)——消息 type / 全局 mode 的合法值。
|
|
21
|
+
# 谁消费:core(全部判定)、engine(信号写点 + 分支)、loop(fatal 文案)、
|
|
22
|
+
# viewer(展示)、fake_agent(responder 决策池)。
|
|
23
|
+
# 为什么建家:这些字符串是**状态机的词表**,任何一处拼写漂移 = 判定静默
|
|
24
|
+
# 失效。曾是 46 处字面量散在 6 个文件(e2e15 自审 S3)。
|
|
25
|
+
# 注意:**不要**为了用常量而替换注释/日志文案里的词(那是给人读的)——
|
|
26
|
+
# 只替换"参与判定的值"。
|
|
27
|
+
# ---------------------------------------------------------------
|
|
28
|
+
T_MESSAGE = "message"
|
|
29
|
+
T_FREEZING = "freezing"
|
|
30
|
+
T_ALL_FREEZING = "all-freezing"
|
|
31
|
+
T_PASS = "pass"
|
|
32
|
+
T_CONCLUDED = "concluded"
|
|
33
|
+
M_MEETING = "meeting"
|
|
34
|
+
M_ROUND_ROBIN = "round-robin"
|
|
35
|
+
M_ALL_FREEZING = T_ALL_FREEZING # mode 值与 type 值同形(af 复用)
|
|
36
|
+
M_CONCLUDED = T_CONCLUDED
|
|
37
|
+
|
|
19
38
|
# ---------------------------------------------------------------
|
|
20
39
|
# 1. 冻结判定(阶段 1.1)
|
|
21
40
|
# ---------------------------------------------------------------
|
|
@@ -187,7 +206,7 @@ def meeting_speak_count(messages, agent):
|
|
|
187
206
|
"meeting 6/2" 的超限假象——6 是消息总数、2 是配额上限)。
|
|
188
207
|
"""
|
|
189
208
|
return sum(1 for fm in messages.get(agent, [])
|
|
190
|
-
if fm.get("mode") ==
|
|
209
|
+
if fm.get("mode") == M_MEETING and fm.get("type") == T_MESSAGE)
|
|
191
210
|
|
|
192
211
|
|
|
193
212
|
def frozen_agents(agents, all_last_types):
|
|
@@ -229,7 +248,7 @@ def can_start_rr(all_last_types):
|
|
|
229
248
|
return False
|
|
230
249
|
if any(t is None for t in all_last_types.values()):
|
|
231
250
|
return False
|
|
232
|
-
return all(t ==
|
|
251
|
+
return all(t == T_ALL_FREEZING for t in all_last_types.values())
|
|
233
252
|
|
|
234
253
|
|
|
235
254
|
def aggregate_mode(all_last):
|
|
@@ -243,13 +262,13 @@ def aggregate_mode(all_last):
|
|
|
243
262
|
4. 否则 → meeting
|
|
244
263
|
"""
|
|
245
264
|
if not all_last:
|
|
246
|
-
return
|
|
265
|
+
return M_MEETING # 空 → 无任何 agent → 无冻结无 RR(review5 F3)
|
|
247
266
|
types = {a: (v["type"] if v else None) for a, v in all_last.items()}
|
|
248
267
|
modes = {a: (v["mode"] if v else None) for a, v in all_last.items()}
|
|
249
|
-
if any(t ==
|
|
250
|
-
return
|
|
251
|
-
if any(m ==
|
|
252
|
-
return
|
|
253
|
-
if all(t in (
|
|
254
|
-
return
|
|
268
|
+
if any(t == T_CONCLUDED for t in types.values()):
|
|
269
|
+
return T_CONCLUDED
|
|
270
|
+
if any(m == M_ROUND_ROBIN for m in modes.values()):
|
|
271
|
+
return M_ROUND_ROBIN
|
|
272
|
+
if all(t in (T_FREEZING, T_ALL_FREEZING) for t in types.values()):
|
|
273
|
+
return T_ALL_FREEZING
|
|
255
274
|
return "meeting"
|