pi-multi-viewers 0.4.0 → 0.5.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/observability.py CHANGED
@@ -120,8 +120,6 @@ def check_status(base):
120
120
  return "not-exists"
121
121
  # 读路径统一走 fs.run_git(quotepath 加固单点;run_cmd 只做一次性
122
122
  # 环境命令——init/clone/config/push)
123
- # 读路径统一走 fs.run_git(quotepath 加固单点;run_cmd 只做一次性
124
- # 环境命令——init/clone/config/push)
125
123
  agents = meeting_fs.read_protocol(bare).get("participants", [])
126
124
  # "收尾完成"判据**单源** = human_viewer.is_finished(concluded 且
127
125
  # HEAD:result.md 有效)——与 viewer 的 done 同一判据(§3.5-P5:此前
@@ -327,6 +325,9 @@ def build_report(base):
327
325
  f"{_dur(d['max_ms'] // 1000)} | rc≠0 {d['fails']} 次")
328
326
 
329
327
  # ---- LLM 运行事实(session 文档化字段;流式预过滤,不整文件解析) ----
328
+ # ---- 扩展策略(声明 vs 生效)----
329
+ _report_extension_line(base, out)
330
+
330
331
  # ---- 终止原因(只报事实与计数)----
331
332
  _report_termination(base, agents, out)
332
333
 
@@ -618,6 +619,46 @@ def _report_termination(base, agents, out):
618
619
  + (" | result.md 已提交" if has_result else ""))
619
620
 
620
621
 
622
+ def _report_extension_line(base, out):
623
+ """扩展策略:**声明 vs 生效**(+ 降级原因)——与"档位:声明 max | 生效 max"同型。
624
+
625
+ 取数(只读已有家,零新增记录):
626
+ · 声明 = `protocol.json.extensionPolicy`(无该字段的旧产物 → n/a)
627
+ · 生效 = loop log 的**登记行**(首唤打一次):
628
+ `扩展策略: 声明=<x> 生效=<y> strict=<0|1>[ 降级原因=<reason>]`
629
+ 为什么需要它:降级本来是**可见的**(loop log 一行),但产品面(报告)看不到
630
+ ——2026-09-14 为确认"MC 生效没有"花了好几轮探针 + DB 查询(e2e24 评审 E2)。
631
+ fail-open:读不到 → 显示 n/a,不报错。
632
+ """
633
+ bare = meeting_fs.bare_of_base(base)
634
+ proto = meeting_fs.read_protocol(bare)
635
+ declared = proto.get("extensionPolicy")
636
+ eff = None
637
+ for f in sorted(glob.glob(os.path.join(base, "loop-*.log"))):
638
+ try:
639
+ txt = open(f, encoding="utf-8", errors="replace").read()
640
+ except OSError:
641
+ continue
642
+ m = re.search(r"扩展策略: 声明=(\S+) 生效=(\S+) strict=(\d)"
643
+ r"(?: 降级原因=(.*))?", txt)
644
+ if m:
645
+ eff = m
646
+ break
647
+ if not declared and not eff:
648
+ out.append("扩展策略:n/a(协议无字段、日志无登记行——多为 2026-09-14 前的产物)")
649
+ return
650
+ if eff:
651
+ d, e, strict, reason = eff.group(1), eff.group(2), eff.group(3), eff.group(4)
652
+ line = f"扩展策略:声明 {d} | 生效 {e}(strict={strict}"
653
+ line += f",降级:{reason.strip()}" if reason else ""
654
+ line += ")"
655
+ if d != e:
656
+ line += " ⚠ 生效≠声明(降级:ctx_search 不可用)"
657
+ out.append(line)
658
+ else:
659
+ out.append(f"扩展策略:声明 {declared} | 生效 n/a(日志中无登记行)")
660
+
661
+
621
662
  def _report_wake_fields(base):
622
663
  """解析 loop-*.log 的登记字段(`elapsed_ms` / `rc`)。
623
664
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-multi-viewers",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Multi-perspective analysis for Pi: fork the main session into N perspective agents over the meeting protocol.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -86,7 +86,7 @@ argument-hint: '"<主题>"'
86
86
  ## 收尾(用户驱动)
87
87
 
88
88
  ```bash
89
- mv.sh --status # done/stopped/running(目录可省略——自动定位当前分析)
89
+ mv.sh --status # 状态 + [result] 路径(目录可省略——自动定位当前分析)
90
90
  ```
91
91
 
92
92
  - `done`:读上一步打印的 `[result]` 路径(产物固定位;**路径由命令给出,
package/spec_gen.py CHANGED
@@ -17,8 +17,7 @@ import sys
17
17
 
18
18
  HERE = os.path.dirname(os.path.abspath(__file__))
19
19
  TPL_DIR = os.path.join(HERE, "templates")
20
- PI_AGENT_DIR = os.environ.get("PI_CODING_AGENT_DIR",
21
- os.path.expanduser("~/.pi/agent"))
20
+ # pi agent 目录的**单一实现**在 meeting_fs.pi_agent_dir()(F9:此前两处各拼一次)
22
21
  MAX_AGENT_NAME_LEN = 32
23
22
 
24
23
  import meeting_fs
@@ -56,7 +55,7 @@ def _default_model():
56
55
  获取失败(pi 不可用/无 settings)→ 返回 None。
57
56
  """
58
57
  try:
59
- with open(os.path.join(PI_AGENT_DIR, "settings.json")) as f:
58
+ with open(os.path.join(meeting_fs.pi_agent_dir(), "settings.json")) as f:
60
59
  cfg = json.load(f)
61
60
  provider = cfg.get("defaultProvider") or ""
62
61
  model = cfg.get("defaultModel") or ""
@@ -123,7 +122,7 @@ def pi_sessions_dir(cwd):
123
122
  AGENTS.md 明言"编码错一根横线 = 静默解析不到"——风险点不应复制。
124
123
  """
125
124
  enc = "--" + cwd.strip("/").replace("/", "-") + "--"
126
- return os.path.join(PI_AGENT_DIR, "sessions", enc)
125
+ return os.path.join(meeting_fs.pi_agent_dir(), "sessions", enc)
127
126
 
128
127
 
129
128
  def current_session_file():
@@ -370,7 +369,8 @@ def gen_question(topic, stances, background, questions):
370
369
  return "\n".join(lines)
371
370
 
372
371
 
373
- def gen_protocol(topic, participants, max_meeting, max_rr, extensions=False,
372
+ def gen_protocol(topic, participants, max_meeting, max_rr,
373
+ extension_policy=meeting_fs.DEFAULT_EXTENSION_POLICY,
374
374
  result_writer=None,
375
375
  stall_timeout=DEFAULT_STALL_TIMEOUT,
376
376
  fork_source=None, fork_cwd=None,
@@ -388,8 +388,12 @@ def gen_protocol(topic, participants, max_meeting, max_rr, extensions=False,
388
388
  "stallTimeoutSeconds": stall_timeout,
389
389
  "commitPolicy": "one-message-per-commit",
390
390
  }
391
- # 显式写出扩展策略(默认零扩展 = agents 不加载任何外部扩展)
392
- proto["extensions"] = bool(extensions)
391
+ # 显式写出扩展策略(值域与默认值的家在 meeting_fs;此处只写选定值)。
392
+ # 非法值就地报错(不落盘半成品)——与 forkMode 同款:配置错误不进运行时
393
+ if extension_policy not in meeting_fs.EXTENSION_POLICIES:
394
+ raise ValueError(f"非法 extension_policy: {extension_policy!r}"
395
+ f"(合法值: {'/'.join(meeting_fs.EXTENSION_POLICIES)})")
396
+ proto["extensionPolicy"] = extension_policy
393
397
  if fork_source:
394
398
  # fork 模式(多视角):首唤挂载主 session + cwd=主项目
395
399
  # forkMode 取值域与默认值的定义在 meeting_fs(FORK_MODES /
@@ -3,7 +3,8 @@
3
3
 
4
4
  用法:
5
5
  python3 start_discussion.py --dir mymeet --topic "主题" --agents a,b \
6
- [--stances '{"a": "立场1", "b": "立场2"}'] [--start] [--extensions] \
6
+ [--stances '{"a": "立场1", "b": "立场2"}'] [--start] \
7
+ [--extension-policy none|mc-tools|all] \
7
8
  [--models '{"a": "provider/model"}'] [--max-meeting 10] [--max-rr 7]
8
9
 
9
10
  复杂内容用 spec 规格目录(设计 16,与 CLI 内容参数互斥):
@@ -350,7 +351,7 @@ def setup_environment(args, participants, base, spec_dir=None,
350
351
  # 共享配置(work-a 提交,setup commit 进 bare)
351
352
  with open(os.path.join(wa, "protocol.json"), "w") as f:
352
353
  json.dump(gen_protocol(spec_topic or args.topic, participants, args.max_meeting,
353
- args.max_rr, args.extensions, args.result_writer,
354
+ args.max_rr, args.extension_policy, args.result_writer,
354
355
  args.stall_timeout,
355
356
  fork_source=getattr(args, "fork_source", None),
356
357
  fork_cwd=os.getcwd(),
@@ -420,7 +421,7 @@ def setup_environment(args, participants, base, spec_dir=None,
420
421
  print(f"[setup] 环境就绪: {base}({len(participants)} agents: {', '.join(participants)})")
421
422
  print(f"[setup] resultWriter={rw}, maxMeeting={args.max_meeting}, maxRR={args.max_rr}, "
422
423
  f"立场={'有' if (args.stances or spec_dir) else '无'}, "
423
- f"extensions={args.extensions}")
424
+ f"extensionPolicy={args.extension_policy}")
424
425
 
425
426
 
426
427
  def _preserve_result_md(base):
@@ -512,7 +513,7 @@ from spec_gen import ( # noqa: F401
512
513
  viewer_set_error, gen_agents_md, gen_protocol,
513
514
  pi_sessions_dir, current_session_file, _join_model_ref,
514
515
  _default_model, _detect_pi_model_thinking, resolve_fork_source,
515
- PI_AGENT_DIR, MAX_AGENT_NAME_LEN,
516
+ MAX_AGENT_NAME_LEN,
516
517
  )
517
518
  from observability import ( # noqa: F401
518
519
  _loop_pids, _loops_alive, check_status, build_report,
@@ -550,9 +551,13 @@ def main():
550
551
  "protocol.json,各 agent 首唤由本地生成 fork "
551
552
  "源挂载主上下文;不传 = 从主 pi 环境自动解析"
552
553
  "(PI_SESSION_ID;解析失败明确报错)")
553
- parser.add_argument("--extensions", action="store_true",
554
- help="让 agents 加载外部扩展(默认零扩展:不加载任何"
555
- "外部扩展/技能/prompt-template/主题)")
554
+ parser.add_argument(
555
+ "--extension-policy",
556
+ choices=list(meeting_fs.EXTENSION_POLICIES),
557
+ default=meeting_fs.DEFAULT_EXTENSION_POLICY,
558
+ help="agents 的扩展策略:mc-tools=默认,只要 MC 的只读检索工具 "
559
+ "ctx_search(缺 MC 时可见降级为零扩展);none=零扩展(零依赖);"
560
+ "all=走 pi 默认发现")
556
561
  parser.add_argument("--start", action="store_true", help="创建后启动讨论")
557
562
  parser.add_argument("--skip-setup", action="store_true",
558
563
  help="跳过环境生成,只启动已有环境(需 --dir)")
@@ -704,12 +709,13 @@ def main():
704
709
  workdir = os.path.join(base, f"work-{p}")
705
710
  cmd = [sys.executable, os.path.join(base, "meeting_loop.py"),
706
711
  workdir, p]
707
- if args.extensions:
708
- cmd.append("--extensions")
712
+ # 扩展策略**不转发**:协议字段(gen_protocol 恒写)是唯一权威,
713
+ # 转发只会多一条通道(S3)。例外语义已写进 decision 20:旧协议
714
+ # 目录(无该字段)按默认档启动。
709
715
  # 配额(max-meeting/max-rr/stall-timeout)是环境属性:创建时
710
716
  # 固化在 protocol.json,启动继承(loop 读 protocol 优先)。
711
717
  # 不传 CLI —— 避免无条件覆盖 protocol.json 的固化值
712
- # (审核 C1:配额单一事实源;与 extensions 处理一致)
718
+ # (审核 C1:配额单一事实源;与 extension-policy 处理一致)
713
719
  with open(os.path.join(base, f"loop-{p}.log"), "w") as f:
714
720
  procs.append(subprocess.Popen(cmd, stdout=f,
715
721
  stderr=subprocess.STDOUT,