pi-multi-viewers 0.3.0 → 0.5.0

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/meeting_loop.py CHANGED
@@ -4,7 +4,7 @@
4
4
  复用 meeting_engine 的唯一状态机,只注入"唤醒 pi"的 responder。
5
5
  协议逻辑(锁/配额/级联/信号)全在引擎,此处只做 LLM 交互。
6
6
 
7
- 用法:python3 meeting_loop.py <workdir> <agent> [--pure]
7
+ 用法:python3 meeting_loop.py <workdir> <agent> [--extension-policy V]
8
8
  (配额/超时从 protocol.json 读——单一事实源;无 CLI 覆盖)
9
9
  """
10
10
 
@@ -289,7 +289,7 @@ def _prepare_fork_session(workdir, agent, sid, fork_source, fork_cwd,
289
289
 
290
290
 
291
291
  def _build_wake_cmd(workdir, agent, sid, cfg, fork_source, fork_cwd,
292
- session_dir, first_wake, pure, prompt,
292
+ session_dir, first_wake, extension_policy, prompt,
293
293
  fork_mode=meeting_fs.DEFAULT_FORK_MODE, topic=""):
294
294
  """组装唤醒命令(#3 拆分,e2e7 评审):返回 (cmd, spawn_cwd)。
295
295
 
@@ -309,11 +309,50 @@ def _build_wake_cmd(workdir, agent, sid, cfg, fork_source, fork_cwd,
309
309
  else:
310
310
  cmd = ["pi", "--mode", "json", "--session-id", sid,
311
311
  "--session-dir", session_dir]
312
- if pure:
313
- # Pi 的 pure 近似:关闭外部扩展/技能/prompt-template/主题加载,
314
- # 保留内置工具(read/bash/edit/write)与项目内 AGENTS.md。
315
- cmd += ["--no-extensions", "--no-skills", "--no-prompt-templates",
316
- "--no-themes"]
312
+ # ---- agent 进程的扩展策略(design.md 决策 20;三档见 meeting_fs)----
313
+ # 三档共用**同一段零扩展前缀**;mc-tools 只决定"能否再追加 -e <MC 入口>"。
314
+ # 为什么这样写:此前 mc-tools 的降级分支自己拼前缀后**提前 return**,把后面的
315
+ # model/thinking/system-prompt/--print 与 spawn cwd 全截断——那条"降级"路径在
316
+ # 无 MC 的机器上产出残缺命令(e2e24 评审 S1;根因 = 前缀抄三遍 + 短路返回)。
317
+ # 现在**只有一个出口**(本函数末尾),降级只是"少追加一个 -e"。
318
+ #
319
+ # 三档语义:mc-tools(默认)给 agents `ctx_search`(MC 的只读检索工具;
320
+ # entry 只注册工具、不装 hook → 不带 historian/压缩);none 零扩展(零依赖);
321
+ # all 走 pi 默认发现(A/B 与显式 opt-in,须有净收益账)。
322
+ no_ext = ["--no-extensions", "--no-skills", "--no-prompt-templates",
323
+ "--no-themes"]
324
+ effective_policy = extension_policy
325
+ downgrade_reason = ""
326
+ if extension_policy not in meeting_fs.EXTENSION_POLICIES:
327
+ # 值域守卫在 __main__ 已拦(配置错误不进 engine);直调也要响
328
+ raise RuntimeError(f"未知 extensionPolicy: {extension_policy!r}"
329
+ f"(合法值: {'/'.join(meeting_fs.EXTENSION_POLICIES)})")
330
+ if extension_policy == "none":
331
+ cmd += no_ext
332
+ elif extension_policy == "mc-tools":
333
+ entry, err = meeting_fs.resolve_mc_tools_entry()
334
+ if entry:
335
+ cmd += no_ext + ["-e", entry]
336
+ elif meeting_fs.mc_tools_strict():
337
+ # 严格模式(测试/探针保真):缺 MC 即响,不降级——否则测试可能在
338
+ # "没装 MC"的环境里通过,而 ctx_search 从未生效
339
+ log(agent, f"[fatal] mc-tools 档入口解析失败(严格模式):{err}")
340
+ raise RuntimeError(f"mc-tools 档不可用: {err}")
341
+ else:
342
+ # mc-tools **允许**而非要求 MC:缺 MC → 降级零扩展,但**可见**
343
+ effective_policy = "none"
344
+ downgrade_reason = err
345
+ cmd += no_ext
346
+ # "all":不加任何 --no-*(走 pi 默认发现)
347
+ if first_wake:
348
+ # 登记行(观测面的稳定字段;报告据此给"声明 vs 生效")。只在首唤打:
349
+ # 策略在一次运行内不变,变了也是配置错误(重跑即可)。
350
+ log(agent, f"扩展策略: 声明={extension_policy} 生效={effective_policy}"
351
+ f" strict={int(meeting_fs.mc_tools_strict())}"
352
+ + (f" 降级原因={downgrade_reason}" if downgrade_reason else ""))
353
+ if downgrade_reason:
354
+ log(agent, f"mc-tools 档未生效({downgrade_reason})——本次按零扩展"
355
+ f"运行:ctx_search 不可用")
317
356
  model = cfg.get("model") or ""
318
357
  if model:
319
358
  cmd += ["--model", model]
@@ -333,6 +372,22 @@ def _build_wake_cmd(workdir, agent, sid, cfg, fork_source, fork_cwd,
333
372
  return cmd, (fork_cwd or workdir)
334
373
 
335
374
 
375
+ def _spawn_env(workdir):
376
+ """agent 进程的环境(Popen 的 env 是**整体替换** → 必须合并 os.environ,
377
+ 否则丢 PATH)。
378
+
379
+ 只注入 `GIT_CEILING_DIRECTORIES=<讨论目录>`:**git 上溯防护**(实现 A1)。
380
+ 为什么需要:`_lock_git` 把 work-<agent>/.git 改名后,git 的默认行为是
381
+ **向上继续找仓库**——fork 模式下 cwd = 主项目,实测锁态下
382
+ `git rev-parse --git-dir` 从 workdir 发起会命中主项目 .git(rc=0),守卫
383
+ 形同虚设(见 `_lock_git` docstring 的范围说明)。本进程的 argv 就是 agent
384
+ 会话里 bash 工具所继承的环境来源。
385
+ (此前还注入 XDG_CONFIG_HOME 做作用域配置——随"屏蔽 AFT"整块退役。)
386
+ """
387
+ env = {**os.environ, "GIT_CEILING_DIRECTORIES": os.path.dirname(workdir)}
388
+ return env
389
+
390
+
336
391
  def _run_wake_proc(cmd, spawn_cwd, workdir, agent):
337
392
  """spawn + 分片等待(#3 拆分):返回 CompletedProcess。
338
393
 
@@ -348,17 +403,12 @@ def _run_wake_proc(cmd, spawn_cwd, workdir, agent):
348
403
  # 成功)。性能实测 ≈150-200 KB/唤醒、峰值亚 MB(不构成风险);
349
404
  # 若改为流式读取,必须让"谁读 session 头"同样显式可见(可读性保留票)。
350
405
  #
351
- # GIT_CEILING_DIRECTORIES=<讨论目录>:**git 上溯防护**(实现 A1)。
352
- # 本进程的 argv 就是 agent 会话里 bash 工具所继承的环境来源——
353
- # 注入后,从 work-<agent> 发起的 git 不会上溯到主项目仓库。
354
- # 为什么需要:_lock_git 把 work-<agent>/.git 改名后,git 的默认行为是
355
- # **向上继续找仓库**——fork 模式下 cwd=主项目,实测锁态下
356
- # `git rev-parse --git-dir` 从 workdir 发起会命中主项目 .git(rc=0),
357
- # 守卫形同虚设(见 _lock_git docstring 的范围说明)。
358
- # 注:Popen 的 env 是**整体替换**,必须合并 os.environ(否则丢 PATH)。
406
+ # 环境构造统一在 _spawn_env(GIT_CEILING_DIRECTORIES 的 git 上溯防护
407
+ # 理由见该函数;brief:本进程的 argv 就是 agent 会话里 bash 工具所继承的
408
+ # 环境来源,而 _lock_git 只锁 work-<agent>/.git、git 默认会向上找仓库)。
359
409
  proc = subprocess.Popen(cmd, cwd=spawn_cwd, stdout=subprocess.PIPE,
360
410
  stderr=subprocess.PIPE, text=True,
361
- env={**os.environ, "GIT_CEILING_DIRECTORIES": base})
411
+ env=_spawn_env(workdir))
362
412
  _current_proc = proc
363
413
  try:
364
414
  # 分片等待:每片检查讨论目录是否被清理(cleanup 删目录)——
@@ -387,7 +437,9 @@ def _run_wake_proc(cmd, spawn_cwd, workdir, agent):
387
437
  err or "")
388
438
 
389
439
 
390
- def wake_llm(workdir, agent, prompt, pure=False, fork_source=None, fork_cwd=None,
440
+ def wake_llm(workdir, agent, prompt,
441
+ extension_policy=meeting_fs.DEFAULT_EXTENSION_POLICY,
442
+ fork_source=None, fork_cwd=None,
391
443
  fork_mode=meeting_fs.DEFAULT_FORK_MODE, topic=""):
392
444
  """唤醒 pi(fork-only:首唤由本地生成 fork 源 + `--session` 打开,
393
445
  后续 `--session-id` 续接)。返回 (sessionID, returncode)。
@@ -412,7 +464,7 @@ def wake_llm(workdir, agent, prompt, pure=False, fork_source=None, fork_cwd=None
412
464
  sid = str(uuid.uuid4())
413
465
  cmd, spawn_cwd = _build_wake_cmd(workdir, agent, sid, cfg, fork_source,
414
466
  fork_cwd, session_dir, first_wake,
415
- pure, prompt, fork_mode, topic)
467
+ extension_policy, prompt, fork_mode, topic)
416
468
 
417
469
  log_dir = os.path.join(base, "wake-logs")
418
470
  os.makedirs(log_dir, exist_ok=True)
@@ -489,7 +541,7 @@ def _read_perspective_brief(workdir, agent):
489
541
  return brief or None
490
542
 
491
543
 
492
- def make_responder(pure, fork_source=None, fork_cwd=None,
544
+ def make_responder(extension_policy, fork_source=None, fork_cwd=None,
493
545
  fork_mode=meeting_fs.DEFAULT_FORK_MODE, topic=""):
494
546
  """构造真实 LLM responder:唤醒 pi,LLM 写内容文件。
495
547
 
@@ -514,7 +566,7 @@ def make_responder(pure, fork_source=None, fork_cwd=None,
514
566
  if mem_available_mb() < MIN_MEM_MB:
515
567
  log(agent, "内存不足——抛可恢复异常(不代写 freezing,下轮重试)")
516
568
  raise RecoverableWakeError("内存不足")
517
- wake_llm(workdir, agent, prompt, pure,
569
+ wake_llm(workdir, agent, prompt, extension_policy,
518
570
  fork_source=fork_source, fork_cwd=fork_cwd)
519
571
  return True
520
572
  if rr_turn:
@@ -534,7 +586,7 @@ def make_responder(pure, fork_source=None, fork_cwd=None,
534
586
  if mem_available_mb() < MIN_MEM_MB:
535
587
  log(agent, "内存不足——抛可恢复异常(不代写 freezing,下轮重试)")
536
588
  raise RecoverableWakeError("内存不足")
537
- wake_llm(workdir, agent, prompt, pure,
589
+ wake_llm(workdir, agent, prompt, extension_policy,
538
590
  fork_source=fork_source, fork_cwd=fork_cwd,
539
591
  fork_mode=fork_mode, topic=topic)
540
592
  return True
@@ -553,11 +605,17 @@ def _preserve_result_md(workdir):
553
605
  if __name__ == "__main__":
554
606
  if len(sys.argv) < 3:
555
607
  print("用法: python3 meeting_loop.py <workdir> <agent> "
556
- "[--max-meeting N] [--max-rr N] [--stall-timeout S] [--pure]")
608
+ "[--max-meeting N] [--max-rr N] [--stall-timeout S]"
609
+ " [--extension-policy none|mc-tools|all]")
557
610
  sys.exit(1)
558
611
  workdir, agent = sys.argv[1], sys.argv[2]
559
612
  recover_git_lock(workdir, agent)
560
- pure = "--pure" in sys.argv
613
+ # 扩展策略:CLI 先填,协议字段(若有)覆盖——与 forkMode 同款
614
+ extension_policy = meeting_fs.DEFAULT_EXTENSION_POLICY
615
+ if "--extension-policy" in sys.argv:
616
+ i = sys.argv.index("--extension-policy")
617
+ if i + 1 < len(sys.argv):
618
+ extension_policy = sys.argv[i + 1]
561
619
  mm, mr, st = 10, 7, 600
562
620
  # 协议从 **bare HEAD** 读(单一来源 = 共享事实;本地副本 LLM 可改)——
563
621
  # 与 engine participants()/check_status 同一原语
@@ -567,8 +625,14 @@ if __name__ == "__main__":
567
625
  print(f"[fatal] protocol.json 读取失败(bare HEAD 无有效内容): {bare}",
568
626
  flush=True)
569
627
  sys.exit(1)
570
- if proto.get("pure"):
571
- pure = True
628
+ if proto.get("extensionPolicy"):
629
+ extension_policy = proto["extensionPolicy"]
630
+ if extension_policy not in meeting_fs.EXTENSION_POLICIES:
631
+ # 值域守卫(与 forkMode 同款四层守卫之一:loop 门)——非法值不进 engine
632
+ # 重试路径,直接 fatal 退出(配置错误就该在启动时响)
633
+ print(f"[fatal] 非法 extensionPolicy: {extension_policy!r}"
634
+ f"(合法值: {'/'.join(meeting_fs.EXTENSION_POLICIES)})", flush=True)
635
+ sys.exit(1)
572
636
  if proto.get("maxMeetingRounds"):
573
637
  mm = proto["maxMeetingRounds"]
574
638
  if proto.get("maxRRRounds"):
@@ -593,7 +657,7 @@ if __name__ == "__main__":
593
657
  sys.exit(1)
594
658
  try:
595
659
  agent_loop(workdir, agent,
596
- make_responder(pure,
660
+ make_responder(extension_policy,
597
661
  fork_source=fork_source,
598
662
  fork_cwd=proto.get("forkCwd") or "",
599
663
  fork_mode=fork_mode_cfg,
package/mv_cli.py CHANGED
@@ -42,6 +42,7 @@ PROG = os.environ.get("MV_INVOKED_AS") or "scripts/mv.sh"
42
42
  USAGE = f"""用法:
43
43
  {PROG} --prepare "<问题>" [--background "<背景>"] [--agents "a,b,c"|4]
44
44
  {PROG} --start <spec目录> [--fork-mode compaction|budget|full]
45
+ {PROG} --start <spec目录> [--extension-policy none|mc-tools|all]
45
46
  {PROG} --status [dir]
46
47
  {PROG} --report [dir]
47
48
  {PROG} --wait [dir]