"""圆桌各角色「单轮 run 参数」的**单一数据源**。 历史问题:前端手动路径(``multi_agent.py`` 的 ``_leader_run`` / ``_special_run``) 和后台进程内网关(``roundtable_inprocess_gateway.py``)各自硬编码了一份「每角色 工具裁剪 + thinking + reasoning」,结果**漂移**出差异(后台席位 / 报告少禁了 ``web_search``,能联网搜索而前端不能)。 本模块把这套策略收敛成唯一定义,两条路径都从这里取,保证「后台挂起」与「前端 手动调用」传参**逐字一致**,且以后改一处即可,不再漂移。 角色(``role``): - ``leader`` —— 总控派活:短决策,关 thinking + low reasoning(首字节快); **只保留 ``agent_orchestration`` / ``ask_clarification`` / ``present_files``**, 其余「干活类」工具(``write_file`` / ``bash`` / ``str_replace`` / ``read_file`` / ``ls`` / ``view_image`` / ``task`` / ``write_todos`` / ``memory`` / ``hindsight_*``) 一律禁用——否则弱模型(如 deepseek-chat)会概率性地**自己写文件、跑 bash 干活** 而不是派活(已在生产日志里观测到总控 ``wc -l outputs/*.md`` 自检自己写的报告)。 总控的唯一职责是「派活 / 澄清 / 收口呈现」,没有任何理由动文件系统或执行命令。 并配合 ``SkillStopMiddleware`` 在 ``agent_orchestration`` 调用后停图,便于网关从 AIMessage.tool_calls 解析派活列表。 - ``seat`` —— 子智能体交付:开 thinking + medium reasoning;**可写文件** (``write_file`` / ``str_replace`` 产出 md 等成品文档)+ **可跑 ``bash``** (2026-06 放开:业务需要跑知识库检索 / Python 辅助分析)。禁派活 / 澄清 / 展示文件 / 看图 / 联网搜索。``report`` 通过 ``[*_SEAT_EXCLUDED, ...]`` 继承本角色, 所以 Step3 报告也跟着拿到 bash;只有 ``leader`` 和 ``seat_parallel`` 仍各自显式禁。 历史警示(保留):弱模型用 ``bash`` 跑 ``python xxx.py > out.md`` 生成产物会 **绕过前端虚拟预览**(预览只拦截 ``write_file.args.content``)→ 沙箱里有文件但 Step2 右侧预览空白。席位 SOUL 必须明确「bash 只用于辅助查询拿数据,**最终交付 必须用 write_file 写 .md 落盘**,不要让脚本生成最终产物」。 - ``seat_parallel`` —— **DAG 快速(fast)模式的并行席位(方案 A,2026-06)**:**禁写文件** (``write_file`` / ``str_replace``,产物走正文)+ **禁 ``bash``** + 禁「打断 / 越权」类, **放开 ``web_search`` 与技能**(搜索不碰 sandbox;技能靠 ``read_file`` SKILL.md)—— 否则并行席位几乎调不动工具、产生不了步骤(步骤条不显示)。 并行席位同时跑 bash 会在共享 sandbox 里抢路径 / 占进程数;且 fast 模式整体不 写文件,bash 跑出来的产物既不能预览也没意义。 「写文件产出 md」归 **沙箱(sandbox)模式**(走 ``seat`` policy,单列 + 沙箱跟随预览); fast 模式快、双列、产物走正文 —— 用户明确要求 fast 不生成文件。详见 ``docs/roundtable-dag-orchestration.md`` §4 / §12.1 / §12.8 / §12.10。 与 ``seat`` 的差异:``seat`` 禁 ``web_search``(旧「圆桌一律禁搜索」), ``seat_parallel`` **放开搜索**(本期诉求);``seat`` 已放开 ``bash``,本角色仍禁。 - ``summary`` —— Step3「总结报告」:写 Markdown 总结报告 + 支持问答/增量改。 - ``action_plan`` —— 岗位会商收口:写 Markdown 行动规划与 JSON 子任务清单。 工具面与 ``report`` 完全一致(继承 ``seat`` 的 bash 放开 + 额外禁 ``str_replace`` / 读前序席位),单列一个角色名便于以后独立调优。 - ``report`` —— Step3 报告绘制:在 ``seat`` 基础上**额外禁 ``str_replace``**: * 继承 ``seat`` 的 bash 放开(2026-06)—— Step3 也可跑 bash 做辅助查询,同 ``seat`` 警示:**结果必须 write_file 落盘**,否则前端虚拟预览会空白 (预览只抓 ``write_file.args.content``,抓不到脚本产物 → 预览停在半截 ``write_file``,正是「没写完就去生成 py 文件」的现象); * 禁 ``str_replace`` → 强制返工用整文件 ``write_file`` 重写 (``str_replace`` 没有 ``content`` 字段,会让前端虚拟预览读到 undefined 而清空沙箱)。 """ from __future__ import annotations from dataclasses import dataclass # ── 每角色基础工具裁剪(唯一定义处)───────────────────────────────────────────── # # 注意:``seat`` / ``report`` 都包含 ``web_search`` —— 圆桌研讨里联网搜索一律禁用 # (与前端 ``_special_run`` 的 excluded_tools 对齐)。 # # ``leader`` 用「白名单」思路反推黑名单:总控**只允许** agent_orchestration(派活)、 # ask_clarification(澄清)、present_files(收口呈现子智能体产出的文件)三件事, # 因此把其余所有工具全部禁掉。关键是 write_file / bash / str_replace —— 留着它们, # 弱模型会绕过派活直接自己写报告(生产日志已实锤)。memory / hindsight_* 一并禁, # 配合 ``memory_injection_enabled=False`` 让单例总控不被上轮会话的记忆带偏席位。 _LEADER_EXCLUDED: list[str] = [ "web_search", "view_image", "bash", "ls", "read_file", "write_file", "str_replace", "task", "write_todos", "memory", "hindsight_recall", "hindsight_reflect", "hindsight_retain", "read_peer_delivery", # 总控靠 _collect_delivery_status 直读各席位,不需要这个席位侧工具 ] # seat:席位**可以写文件**(write_file / str_replace)产出 md 等成品文档,**bash 放开** # (2026-06 决策,业务需要 bash 跑知识库检索 / Python 辅助分析)。禁派活 / 澄清 / # 展示文件 / 看图 / 联网搜索。``report`` 通过 ``[*_SEAT_EXCLUDED, ...]`` 继承本角色, # 所以 Step3 报告跟着拿到 bash;只有 leader / seat_parallel 仍各自显式禁。 # # **历史警示(保留)**:弱模型用 bash 跑 ``python xxx.py > out.md`` 生成最终产物会 # 绕过前端虚拟预览(只拦 write_file.args.content) → 沙箱里有文件但 Step2 右侧预览 # 空白。必须靠席位 SOUL 强约束「bash 只用于辅助查询拿数据,最终交付必须用 write_file # 写 .md 落盘」。 _SEAT_EXCLUDED: list[str] = [ "web_search", "ask_clarification", "present_files", "view_image", "agent_orchestration", ] # 并行席位 = **快速(fast)模式**(双列):**禁写文件**(产物走正文)+ 禁跑命令 +「打断/越权」类。 # **禁 web_search** —— 离线内网部署无外网,席位检索一律走「技能(skill)」(靠 read_file 读 # SKILL.md,不联网),与 leader / seat / report 三角色保持一致(2026-06 离线部署决策)。 # 「写文件」归 **沙箱(sandbox)模式**(seat policy,单列 + 沙箱跟随预览);fast 模式快、双列、 # 产物走正文 —— 用户明确要求 fast 不生成文件(2026-06 §12.10)。 _SEAT_PARALLEL_EXCLUDED: list[str] = [ "web_search", # 离线部署无外网,检索走技能(skill),不联网 "ask_clarification", # 并行批次不打断问澄清 "present_files", # 产物走正文,不呈现文件 "view_image", "agent_orchestration", # 席位不派活 "bash", # 禁跑脚本(防 .py 脚本生成产物) "write_file", # fast 模式产物走正文,不写文件(写文件归 sandbox 模式) "str_replace", ] # report:在 seat 基础上额外禁 str_replace(bash 已在 seat 禁)——强制整文件 write_file 重写, # 让前端虚拟预览能拿到完整 args.content。report 是 Step3 单独绘制,不读前序席位 → 禁 read_peer_delivery。 _REPORT_EXCLUDED: list[str] = [*_SEAT_EXCLUDED, "str_replace", "read_peer_delivery"] # summary:Step3「总结报告」智能体(写 Markdown 报告 + 支持问答/增量改)。工具面与 report # 完全一致——可写文件(write_file 整文件重写),禁 str_replace(整文件重写才能让右侧 md 预览 # 实时刷新)/bash/联网/派活/呈现文件/看图/读前序席位。单列出来便于以后独立调优。 _SUMMARY_EXCLUDED: list[str] = [*_SEAT_EXCLUDED, "str_replace", "read_peer_delivery"] # 岗位会商行动规划与总结报告一样:只能归纳已给出的材料,最终交付必须直接 write_file # 到 outputs,不能联网、派活或改动其他线程的文件。 _ACTION_PLAN_EXCLUDED: list[str] = [*_SUMMARY_EXCLUDED] # ingest:Step3「入库」——让 roundtable-structure 真正**调用入库技能**把方案成果(总结报告 + # 结构化流程图 + taskId)沉淀进知识库。和 dashboard 相反:dashboard 禁一切工具(只输出 JSON), # ingest 必须**放开技能执行链路**——技能靠 ``read_file`` 读 SKILL.md、按其指令用 ``bash`` / # ``write_file`` 等工具落地入库(如 curl 入库接口、写中转文件)。在 ``seat`` 基础上额外禁: # ``str_replace``(整文件重写即可)、``read_peer_delivery``(单轮入库不读前序席位)、``task`` # (不派子代理)、``write_todos`` / ``memory`` / ``hindsight_*``(单例 agent 不吃跨会话记忆, # 避免污染)。保留 ``read_file`` / ``write_file`` / ``bash`` / ``ls`` 让技能能真正执行。 # thinking + medium:需要先判断该用哪个入库技能、再按其契约调用。 _INGEST_EXCLUDED: list[str] = [ *_SEAT_EXCLUDED, "str_replace", "read_peer_delivery", "task", "write_todos", "memory", "hindsight_recall", "hindsight_reflect", "hindsight_retain", ] # dashboard:Step3「大屏多页」数据智能体(roundtable-dashboard)——纯「研讨成果 → report-json」 # 提炼器,**禁用一切工具**(同 leader 的黑名单全集)。它只需读 prompt、分析各席位、在对话里 # 输出一个 report-json 代码块;留任何干活类工具(尤其 write_file / bash)只会让弱模型跑偏去 # 写文件而不输出 JSON(report agent 上已实锤这类行为)。thinking + medium:需要逐席位分析归纳。 _DASHBOARD_EXCLUDED: list[str] = [ "web_search", "view_image", "bash", "ls", "read_file", "write_file", "str_replace", "task", "write_todos", "memory", "hindsight_recall", "hindsight_reflect", "hindsight_retain", "read_peer_delivery", "agent_orchestration", "ask_clarification", "present_files", ] # leader 内禀的 skill_stop:``agent_orchestration`` 调用后停图。调用方可在此基础上 # 再叠加自己的 skill_stop(见 ``merge_skill_stop_names``)。 _LEADER_SKILL_STOP: list[str] = ["agent_orchestration"] @dataclass(frozen=True) class RoundtableRunPolicy: """一个角色单轮 run 的归一化参数。 与 ``multi_agent._run_payload`` 的入参一一对应:``excluded_tools`` / ``skill_stop_names`` / ``thinking_enabled`` / ``reasoning_effort``。 """ excluded_tools: list[str] skill_stop_names: list[str] thinking_enabled: bool reasoning_effort: str def roundtable_run_policy(role: str) -> RoundtableRunPolicy: """返回某角色(``leader`` / ``seat`` / ``seat_parallel`` / ``report``)的单轮 run 参数。 返回值里的列表是**新副本**,调用方可安全地再 append/merge 而不污染本模块常量。 未知角色抛 ``ValueError``(早失败,避免悄悄退化到错误的工具面)。 """ if role == "leader": return RoundtableRunPolicy( excluded_tools=list(_LEADER_EXCLUDED), skill_stop_names=list(_LEADER_SKILL_STOP), thinking_enabled=False, reasoning_effort="low", ) if role == "seat": return RoundtableRunPolicy( excluded_tools=list(_SEAT_EXCLUDED), skill_stop_names=[], thinking_enabled=True, reasoning_effort="medium", ) if role == "seat_parallel": # 同 seat(thinking + medium),但额外禁写文件 / 跑命令(防并行沙箱打架)。 return RoundtableRunPolicy( excluded_tools=list(_SEAT_PARALLEL_EXCLUDED), skill_stop_names=[], thinking_enabled=True, reasoning_effort="medium", ) if role == "report": return RoundtableRunPolicy( excluded_tools=list(_REPORT_EXCLUDED), skill_stop_names=[], thinking_enabled=True, reasoning_effort="medium", ) if role == "summary": return RoundtableRunPolicy( excluded_tools=list(_SUMMARY_EXCLUDED), skill_stop_names=[], thinking_enabled=True, reasoning_effort="medium", ) if role == "action_plan": return RoundtableRunPolicy( excluded_tools=list(_ACTION_PLAN_EXCLUDED), skill_stop_names=[], thinking_enabled=True, reasoning_effort="medium", ) if role == "dashboard": return RoundtableRunPolicy( excluded_tools=list(_DASHBOARD_EXCLUDED), skill_stop_names=[], thinking_enabled=True, reasoning_effort="medium", ) if role == "ingest": return RoundtableRunPolicy( excluded_tools=list(_INGEST_EXCLUDED), skill_stop_names=[], thinking_enabled=True, reasoning_effort="medium", ) raise ValueError( f"unknown roundtable role: {role!r} (expected leader/seat/seat_parallel/report/summary/action_plan/dashboard/ingest)" ) def merge_skill_stop_names(policy_names: list[str], caller_names: list[str] | None) -> list[str]: """把角色内禀 skill_stop 与调用方追加的 skill_stop 合并去重(保序)。 取代历史上 ``list({"agent_orchestration", *skill_stop_names})`` 的 set 写法 (set 顺序不确定);这里用 dict.fromkeys 保序去重,结果确定、便于断言。 """ return list(dict.fromkeys([*policy_names, *(caller_names or [])]))