codebee 0.1.7 → 0.1.8
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/CHANGELOG.md +10 -0
- package/README.md +236 -252
- package/app/core/catalog.py +24 -5
- package/app/core/errorlog.py +179 -0
- package/app/core/flows.py +6 -2
- package/app/core/gitmod.py +45 -1
- package/app/core/health.py +48 -11
- package/app/core/jobs.py +104 -9
- package/app/core/manager.py +21 -0
- package/app/core/modelhub.py +80 -11
- package/app/core/paths.py +1 -0
- package/app/core/pipeline.py +420 -88
- package/app/core/runner.py +122 -10
- package/app/core/settings.py +9 -2
- package/app/core/step_runner.py +28 -4
- package/app/core/store.py +180 -139
- package/app/core/telemetry.py +291 -0
- package/app/core/token_meter.py +18 -0
- package/app/main.py +162 -16
- package/app/pick_dialog.py +78 -0
- package/app/ui/app.js +978 -601
- package/app/ui/i18n.js +77 -6
- package/app/ui/index.html +161 -80
- package/app/ui/style.css +827 -67
- package/package.json +1 -1
package/app/core/pipeline.py
CHANGED
|
@@ -79,6 +79,35 @@ def _agents():
|
|
|
79
79
|
return registry.effective_agents(catalog.load(), manager.detect_all())
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
# 本轮运行的智能体池:execute_run 入口快照,_run_step 死链补位时扫描。
|
|
83
|
+
# 直接调 _run_step 的场景(单测/内部工具)池为空 → 补位不触发,闸门语义不变。
|
|
84
|
+
_CURRENT_AGENTS: list = []
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _dead_binding_substitute(dead_id, resume=None):
|
|
88
|
+
"""死链补位:原定 CLI 配了链但解析为空(供应商停删/无密钥/**协议不匹配**——
|
|
89
|
+
如 chat-only 供应商挂在只讲 responses 的 codex 链上)时,从同池找一个
|
|
90
|
+
「配置过且链还活着」的真实 CLI 顶上——用户配的其它供应商继续干活,
|
|
91
|
+
而不是整步判死。全池无活链才维持失败:死链闸门「绝不静默偷跑本机默认」
|
|
92
|
+
的语义不变,补位用的仍是用户显式配好的链。
|
|
93
|
+
|
|
94
|
+
resume 会话钉在原 CLI 上(会话跟人走),有 resume 时补位无意义,直接不找。
|
|
95
|
+
返回 bind_agent 之后的替代者,或 None。"""
|
|
96
|
+
if resume:
|
|
97
|
+
return None
|
|
98
|
+
from . import modelhub as _mh
|
|
99
|
+
for a in _CURRENT_AGENTS or []:
|
|
100
|
+
if a.get("id") == dead_id or a.get("mode") != "real":
|
|
101
|
+
continue
|
|
102
|
+
try:
|
|
103
|
+
cand = _mh.bind_agent(a, "default")
|
|
104
|
+
except Exception:
|
|
105
|
+
continue
|
|
106
|
+
if cand.get("binding_configured") and (cand.get("call_chain") or cand.get("env")):
|
|
107
|
+
return cand
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
82
111
|
def _pick(agents, agent_id):
|
|
83
112
|
for a in agents:
|
|
84
113
|
if a["id"] == agent_id:
|
|
@@ -290,13 +319,23 @@ def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner
|
|
|
290
319
|
# · 从没配过链(binding_configured=False):回落 CLI 本机默认照跑——
|
|
291
320
|
# 用户根本没在 CodeBee 里配供应商,谈不到「烧自己配的配额」;判失败
|
|
292
321
|
# 反而把用本地登录的普通用户全挡在门外(0.1.6 真实装机误伤案例)。
|
|
293
|
-
# · 配过链但全死(binding_configured=True
|
|
294
|
-
#
|
|
295
|
-
#
|
|
296
|
-
#
|
|
322
|
+
# · 配过链但全死(binding_configured=True):先看同池有没有「配置过且链还
|
|
323
|
+
# 活着」的 CLI 可补位(评审补位的全步骤版——用户的其它供应商继续干活,
|
|
324
|
+
# 典型场景:chat-only 供应商挂在只讲 responses 的 codex 链上必然解析为
|
|
325
|
+
# 空,2026-09-18 重写任务 3/3 续跑全灭案);全池无活链才判失败不静默
|
|
326
|
+
# 降级——宁可失败不偷跑本机默认。只记在步骤备注/错误里,不产生健康胶囊。
|
|
297
327
|
dead_binding = (agent.get("mode") == "real"
|
|
298
328
|
and agent.get("binding_configured")
|
|
299
329
|
and not (agent.get("call_chain") or agent.get("env")))
|
|
330
|
+
if dead_binding:
|
|
331
|
+
sub = _dead_binding_substitute(agent.get("id"), resume=resume)
|
|
332
|
+
if sub is not None:
|
|
333
|
+
note = ((note + ";") if note else "") + (
|
|
334
|
+
"⚠ 原定 %s 绑定链全部失效,已补位 %s"
|
|
335
|
+
% (agent.get("label") or agent["id"],
|
|
336
|
+
sub.get("label") or sub["id"]))
|
|
337
|
+
agent = sub
|
|
338
|
+
dead_binding = False
|
|
300
339
|
dead_msg = _binding_dead_msg(agent) if dead_binding else ""
|
|
301
340
|
if dead_binding:
|
|
302
341
|
note = ((note + ";") if note else "") + "⚠ " + dead_msg
|
|
@@ -353,12 +392,15 @@ def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner
|
|
|
353
392
|
return res
|
|
354
393
|
|
|
355
394
|
|
|
356
|
-
def _run_builtin_step(run_id, role, bi, prompt, workdir, ev, note="", images=None
|
|
395
|
+
def _run_builtin_step(run_id, role, bi, prompt, workdir, ev, note="", images=None,
|
|
396
|
+
followups=False):
|
|
357
397
|
"""内置智能体步骤:直连模型 API + 工具循环(builtin_agent),不经 CLI 进程。
|
|
358
398
|
|
|
359
399
|
与 _run_step 对齐的三件事:暂停/取消闸门、运行中指令 drain 注入、重复调用
|
|
360
400
|
守门;结果同样经 _finish_step_result 落步骤(output=干净回答)并入用量台账。
|
|
361
|
-
日志只有「迭代/工具」摘要行——对话视图吃 output,日志抽屉看工具轨迹。
|
|
401
|
+
日志只有「迭代/工具」摘要行——对话视图吃 output,日志抽屉看工具轨迹。
|
|
402
|
+
followups=True 时从回答末尾解析「建议追问」块(直连对话专用协议):
|
|
403
|
+
剥离出结构化列表落步骤记录,正文保持干净。"""
|
|
362
404
|
_wait_gate(run_id, ev)
|
|
363
405
|
step, log_abs = store.add_step(run_id, role, "builtin", "CodeBee", note=note)
|
|
364
406
|
start = time.time()
|
|
@@ -387,6 +429,11 @@ def _run_builtin_step(run_id, role, bi, prompt, workdir, ev, note="", images=Non
|
|
|
387
429
|
lines.append(str(line))
|
|
388
430
|
|
|
389
431
|
res = builtin_agent.run(bi, prompt, workdir, cancel_event=ev, log=_log, images=images)
|
|
432
|
+
if followups and res.get("ok"):
|
|
433
|
+
clean, fups = _parse_followups(res.get("text") or "")
|
|
434
|
+
if fups:
|
|
435
|
+
res["text"] = clean
|
|
436
|
+
res["followups"] = fups
|
|
390
437
|
if log_abs:
|
|
391
438
|
try:
|
|
392
439
|
log_abs.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
@@ -499,7 +546,23 @@ def _finish_step_result(run_id, step, res, role, agent, start):
|
|
|
499
546
|
model=res.get("model"),
|
|
500
547
|
# 智能体的最终回答(runner 已从 JSONL 事件流里抽出 agent_message)。
|
|
501
548
|
# 对话视图直读这个;日志文件是全量事件流,塞进气泡就成了「看日志」。
|
|
502
|
-
output=(res.get("text") or "")
|
|
549
|
+
output=(res.get("text") or ""),
|
|
550
|
+
followups=res.get("followups"))
|
|
551
|
+
# 错误台账:失败/超时各记一条结构化记录(遥测与诊断包的数据源)。
|
|
552
|
+
# 用户主动取消不入账——那不是产品问题;detail 只存脱敏后的失败摘录。
|
|
553
|
+
if status in ("failed", "timeout"):
|
|
554
|
+
try:
|
|
555
|
+
from . import errorlog
|
|
556
|
+
prov = agent.get("provider") or {}
|
|
557
|
+
errorlog.record(
|
|
558
|
+
category="step", reason=str(res.get("error_code") or "UNKNOWN"),
|
|
559
|
+
detail=(res.get("error") or ""),
|
|
560
|
+
provider=(prov.get("name") if isinstance(prov, dict) else "") or "",
|
|
561
|
+
model=res.get("model") or "", tool=agent.get("kind", ""),
|
|
562
|
+
role=role, run_id=run_id, task_id=(store.get_run(run_id) or {}).get("task_id") or "",
|
|
563
|
+
step=step["n"], exit_code=res.get("raw", {}).get("exit_code"))
|
|
564
|
+
except Exception:
|
|
565
|
+
pass
|
|
503
566
|
if agent.get("mode") != "mock":
|
|
504
567
|
_record_usage(run_id, role, agent, res, source="pipeline", step=step["n"])
|
|
505
568
|
# 5F:step 级运行时断言(只告警不阻断)
|
|
@@ -915,8 +978,9 @@ __CONTEXT__
|
|
|
915
978
|
|
|
916
979
|
## 要求
|
|
917
980
|
- 能改直接改、能写直接写(限本工作目录内),产出文件一律 UTF-8 编码(PowerShell 写文件显式 -Encoding UTF8)。
|
|
981
|
+
- 正文直接交代结果与答案:不要写「本轮做了什么」这类开场总结,也不要「回复:」这类引导词。
|
|
918
982
|
- 回复的最后一行单独输出一行交代结果:
|
|
919
|
-
DIRECT_DONE:
|
|
983
|
+
DIRECT_DONE: <一句话结果摘要(含产出文件)>
|
|
920
984
|
这一行之后不要再输出任何内容。"""
|
|
921
985
|
|
|
922
986
|
DIRECT_FOLLOWUP_PROMPT = """你在与用户的持续对话中。用户针对已有成果发来了新消息(见下方「用户实时指令」注入块),请接着处理。
|
|
@@ -926,8 +990,9 @@ __GOAL__
|
|
|
926
990
|
|
|
927
991
|
## 要求
|
|
928
992
|
- 优先回应用户新消息(继续做/改/答疑均可),仍限本工作目录内。
|
|
993
|
+
- 正文开门见山直接回答:不要写「本轮做了什么」这类开场总结,也不要「回复:」这类引导词。
|
|
929
994
|
- 回复的最后一行单独输出:
|
|
930
|
-
DIRECT_DONE:
|
|
995
|
+
DIRECT_DONE: <一句话结果摘要>
|
|
931
996
|
这一行之后不要再输出任何内容。"""
|
|
932
997
|
|
|
933
998
|
DIRECT_MAX_TURNS = 200 # 对话续轮上限(每轮都要用户主动发消息才触发,防意外打满)
|
|
@@ -942,7 +1007,8 @@ __CONTEXT__
|
|
|
942
1007
|
|
|
943
1008
|
## 要求
|
|
944
1009
|
- 能改直接改、能写直接写(用工具,限本工作目录内),产出文件一律 UTF-8 编码。
|
|
945
|
-
-
|
|
1010
|
+
- 完成后直接给用户结论与答案,需要时顺带交代产出/修改了哪些文件;不要写「本轮做了什么」这类开场白。
|
|
1011
|
+
__FOLLOWUPS__"""
|
|
946
1012
|
|
|
947
1013
|
BUILTIN_FOLLOWUP_PROMPT = """## 原始任务
|
|
948
1014
|
__GOAL__
|
|
@@ -952,7 +1018,48 @@ __PREV__
|
|
|
952
1018
|
|
|
953
1019
|
## 要求
|
|
954
1020
|
- 优先回应用户的新消息(继续做/改/答疑均可),仍限本工作目录内,工具可用。
|
|
955
|
-
-
|
|
1021
|
+
- 直接给答案/结果,需要时再带一句改动说明;不要写「本轮做了什么」这类开场白,也不要「回复:」这类引导词。
|
|
1022
|
+
__FOLLOWUPS__"""
|
|
1023
|
+
|
|
1024
|
+
# 追问建议协议(借鉴 freebuff 的 suggest_followups):模型在正文后自带最多 3 条
|
|
1025
|
+
# 建议追问,后端解析成结构化字段、从正文剥离,前端渲染成可点芯片。省一次额外
|
|
1026
|
+
# 请求;模型不配合时自然没有芯片,无需兜底。
|
|
1027
|
+
FOLLOWUPS_PROTOCOL = """
|
|
1028
|
+
## 回复末尾协议
|
|
1029
|
+
正文全部写完之后,另起一段输出最多 3 条「建议追问」(用户最可能接着问的方向),格式严格如下;没有合适的方向就整个省略,绝不要输出空的或凑数的:
|
|
1030
|
+
<followups>
|
|
1031
|
+
10 字内的短标签 | 用户点击后会原样发送的完整消息(第一人称,一句话)
|
|
1032
|
+
</followups>"""
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
def _parse_followups(text):
|
|
1036
|
+
"""从回答正文里解析并剥离 <followups> 块。返回 (干净正文, [建议列表])。
|
|
1037
|
+
|
|
1038
|
+
每行「标签 | 消息」(兼容全角|),最多取 3 条,两端空白与空行忽略;
|
|
1039
|
+
没有块或解析不出任何合法行时原样返回。"""
|
|
1040
|
+
import re as _re
|
|
1041
|
+
if not text or "<followups>" not in text:
|
|
1042
|
+
return text, []
|
|
1043
|
+
m = _re.search(r"<followups>\s*([\s\S]*?)</followups>", text)
|
|
1044
|
+
if not m:
|
|
1045
|
+
return text, []
|
|
1046
|
+
out = []
|
|
1047
|
+
for line in m.group(1).splitlines():
|
|
1048
|
+
line = line.strip().lstrip("-•* ").strip()
|
|
1049
|
+
if not line:
|
|
1050
|
+
continue
|
|
1051
|
+
parts = _re.split(r"[||]", line, 1)
|
|
1052
|
+
if len(parts) != 2:
|
|
1053
|
+
continue
|
|
1054
|
+
label = parts[0].strip()
|
|
1055
|
+
msg = parts[1].strip()
|
|
1056
|
+
if not label or not msg:
|
|
1057
|
+
continue
|
|
1058
|
+
out.append({"label": label[:24], "prompt": msg[:200]})
|
|
1059
|
+
if len(out) >= 3:
|
|
1060
|
+
break
|
|
1061
|
+
clean = (text[:m.start()] + text[m.end():]).strip()
|
|
1062
|
+
return clean, out
|
|
956
1063
|
|
|
957
1064
|
|
|
958
1065
|
def _pending_messages(run_id):
|
|
@@ -1050,7 +1157,8 @@ def _run_direct(run, task, agents, ev, stats, mode):
|
|
|
1050
1157
|
if bi is not None:
|
|
1051
1158
|
prompt = (BUILTIN_DIRECT_PROMPT
|
|
1052
1159
|
.replace("__GOAL__", task["goal"])
|
|
1053
|
-
.replace("__CONTEXT__", task.get("context") or "(无)")
|
|
1160
|
+
.replace("__CONTEXT__", task.get("context") or "(无)")
|
|
1161
|
+
.replace("__FOLLOWUPS__", FOLLOWUPS_PROTOCOL))
|
|
1054
1162
|
else:
|
|
1055
1163
|
prompt = (DIRECT_PROMPT
|
|
1056
1164
|
.replace("__GOAL__", task["goal"])
|
|
@@ -1061,7 +1169,8 @@ def _run_direct(run, task, agents, ev, stats, mode):
|
|
|
1061
1169
|
if bi is not None:
|
|
1062
1170
|
prompt = (BUILTIN_FOLLOWUP_PROMPT
|
|
1063
1171
|
.replace("__GOAL__", task["goal"])
|
|
1064
|
-
.replace("__PREV__", (last_text or "(无)")[-3000:])
|
|
1172
|
+
.replace("__PREV__", (last_text or "(无)")[-3000:])
|
|
1173
|
+
.replace("__FOLLOWUPS__", FOLLOWUPS_PROTOCOL))
|
|
1065
1174
|
else:
|
|
1066
1175
|
prompt = DIRECT_FOLLOWUP_PROMPT.replace("__GOAL__", task["goal"])
|
|
1067
1176
|
if not sid and last_text:
|
|
@@ -1077,7 +1186,8 @@ def _run_direct(run, task, agents, ev, stats, mode):
|
|
|
1077
1186
|
before_n = len(_pending_messages(run_id))
|
|
1078
1187
|
if bi is not None:
|
|
1079
1188
|
res = _run_builtin_step(run_id, "direct" if first else "chat", bi, prompt,
|
|
1080
|
-
step_wd, ev=ev, note=note, images=images
|
|
1189
|
+
step_wd, ev=ev, note=note, images=images,
|
|
1190
|
+
followups=True)
|
|
1081
1191
|
else:
|
|
1082
1192
|
res = _run_step(run_id, "direct" if first else "chat", impl, prompt, step_wd,
|
|
1083
1193
|
readonly=False, ev=ev, note=note,
|
|
@@ -1292,6 +1402,24 @@ def _read_text_any_enc(p):
|
|
|
1292
1402
|
return runner.read_text_any_enc(p)
|
|
1293
1403
|
|
|
1294
1404
|
|
|
1405
|
+
def _critique_json(res, dims):
|
|
1406
|
+
"""评审输出解析三道网,返回统一形状 {scores, issues, summary}:
|
|
1407
|
+
① extract_json(严格 JSON → 围栏 → 宽松修复内嵌引号 → 花括号扫描)
|
|
1408
|
+
② as_scores(兜底扫描掉进内层时,返回值本身就是 维度→分 本体,包回)
|
|
1409
|
+
③ scores_from_prose(agentic CLI 把 JSON 写进文件、stdout 只留中文总结)
|
|
1410
|
+
任何一道出分即算有效评审——「无法解析」绝不能把正常出分的评审吞掉
|
|
1411
|
+
(2026-09-18 七猫案:kimi 内嵌引号病连烧三轮自动续跑全判评审全挂)。"""
|
|
1412
|
+
text = res.get("text") or ""
|
|
1413
|
+
gj = runner.as_scores(runner.extract_json(text))
|
|
1414
|
+
if isinstance(gj, dict) and isinstance(gj.get("scores"), dict) and gj.get("scores"):
|
|
1415
|
+
return gj
|
|
1416
|
+
prose = runner.scores_from_prose(text, dims)
|
|
1417
|
+
if prose:
|
|
1418
|
+
return {"scores": prose, "issues": [], "summary": text[:400]}
|
|
1419
|
+
return {"scores": {}, "issues": [],
|
|
1420
|
+
"summary": "评审输出无法解析:%s" % (text or res.get("error") or "")[:150]}
|
|
1421
|
+
|
|
1422
|
+
|
|
1295
1423
|
def _chapter_io(workdir, i, mode):
|
|
1296
1424
|
"""打开第 i 章文件;open 紧邻边界校验,路径越界直接拒绝(形态同 _ms_io)。
|
|
1297
1425
|
读模式兼容 GBK 落盘的章稿(见 _read_text_any_enc)。"""
|
|
@@ -1515,13 +1643,8 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
1515
1643
|
if lens else "") + note_extra,
|
|
1516
1644
|
workdir, readonly=True, ev=ev,
|
|
1517
1645
|
resume=sids.get(agent["id"]))
|
|
1518
|
-
cj =
|
|
1519
|
-
if
|
|
1520
|
-
or not cj.get("scores"):
|
|
1521
|
-
cj = {"scores": {}, "issues": [],
|
|
1522
|
-
"summary": "评审输出无法解析:%s" % (res.get("text")
|
|
1523
|
-
or res.get("error") or "")[:150]}
|
|
1524
|
-
else:
|
|
1646
|
+
cj = _critique_json(res, dims)
|
|
1647
|
+
if cj.get("scores"):
|
|
1525
1648
|
scored += 1
|
|
1526
1649
|
# §07 T1.1:记录该评审的会话 id(第 2 轮复用)
|
|
1527
1650
|
csid = _resume_sid(agent, res.get("sid"))
|
|
@@ -1870,40 +1993,70 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
1870
1993
|
|
|
1871
1994
|
# ---- 3) 全局一致性评审(覆盖 1..end 全书:续写批次必须连同旧章一起查一致性)
|
|
1872
1995
|
full_text = "\n\n".join(_read_chapter(workdir, i) for i in range(1, end + 1))
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1996
|
+
global_issues = []
|
|
1997
|
+
|
|
1998
|
+
def run_global_round(agent_list):
|
|
1999
|
+
"""一轮全局评审:返回 (出分评审数, 按维累计分)。失败/不可解析不得当成低分计入。"""
|
|
2000
|
+
gmeans_acc, scored = {}, 0
|
|
2001
|
+
for agent in agent_list:
|
|
2002
|
+
if agent.get("mode") == "mock":
|
|
2003
|
+
step, _ = store.add_step(run_id, "global-critique", agent["id"],
|
|
2004
|
+
agent.get("label"))
|
|
2005
|
+
time.sleep(0.15)
|
|
2006
|
+
gj = {"scores": {d: 8.0 for d in dims},
|
|
2007
|
+
"issues": [], "summary": "(mock)全书结构完整,达到可签约水平"}
|
|
2008
|
+
store.finish_step(run_id, step["n"], "done", summary="均分 8.0:(mock)全书达标",
|
|
2009
|
+
duration_s=0.15)
|
|
2010
|
+
scored += 1
|
|
2011
|
+
else:
|
|
2012
|
+
gtpl = SERIAL_GLOBAL_PROMPT
|
|
2013
|
+
if bible:
|
|
2014
|
+
gtpl = gtpl.replace("## 全书目标", bible + "\n\n## 全书目标", 1)
|
|
2015
|
+
res = _run_step(run_id, "global-critique", modelhub.bind_agent(agent, difficulty),
|
|
2016
|
+
(gtpl.replace("__DIMKEYS__", dimkey)
|
|
2017
|
+
.replace("__GOAL__", task["goal"])
|
|
2018
|
+
.replace("__MANUSCRIPT__", full_text[:60000])),
|
|
2019
|
+
workdir, readonly=True, ev=ev, timeout=2400)
|
|
2020
|
+
gj = _critique_json(res, dims)
|
|
2021
|
+
if gj.get("scores"):
|
|
2022
|
+
scored += 1
|
|
2023
|
+
global_issues.extend({"chapter": "全书", **it} for it in (gj.get("issues") or [])[:8])
|
|
2024
|
+
for d in dims:
|
|
2025
|
+
v = gj.get("scores", {}).get(d)
|
|
2026
|
+
if v is not None:
|
|
2027
|
+
gmeans_acc.setdefault(d, []).append(float(v))
|
|
2028
|
+
_check_cancel(ev)
|
|
2029
|
+
return scored, gmeans_acc
|
|
2030
|
+
|
|
2031
|
+
gscored, gmeans_acc = run_global_round(critics)
|
|
2032
|
+
# 「评不上」≠「评了低分」:全局评审全挂时先从其它真实智能体补位(对齐章级
|
|
2033
|
+
# 评审者级 fallback);补位后仍零分则判 run 失败——「无法评审」绝不能当成
|
|
2034
|
+
# 「全局评审未通过」去盖「未达标」章(2026-09-18 假未达标案:codex 绑定链
|
|
2035
|
+
# 全失效 + kimi 命令行超长,global_scores 为空被 _all_ge 判成不通过)。
|
|
2036
|
+
# mock 评审总出分,不会误触;判失败不设 impl mock 例外(对齐章级中止)。
|
|
2037
|
+
if not gscored:
|
|
2038
|
+
tried = {a.get("id") for a in critics}
|
|
2039
|
+
for spare in [a for a in (agents or [])
|
|
2040
|
+
if a.get("mode") == "real" and a.get("id") not in tried][:2]:
|
|
2041
|
+
sc, acc = run_global_round([spare])
|
|
2042
|
+
for d, xs in acc.items():
|
|
2043
|
+
gmeans_acc.setdefault(d, []).extend(xs)
|
|
2044
|
+
gscored += sc
|
|
2045
|
+
if gscored:
|
|
2046
|
+
break
|
|
2047
|
+
if not gscored:
|
|
2048
|
+
store.update_run(run_id, status="failed",
|
|
2049
|
+
error="全局一致性评审全部失败(评审模型不可用或输出不可解析),"
|
|
2050
|
+
"已中止以免把「无法评审」误判为「未达标」。"
|
|
2051
|
+
"各章稿件已全部落盘,修复评审链后续跑可直接收尾",
|
|
2052
|
+
ended_at=_now())
|
|
2053
|
+
return
|
|
2054
|
+
global_means = {d: round(sum(xs) / len(xs), 1) for d, xs in gmeans_acc.items()}
|
|
1902
2055
|
global_pass = _all_ge(global_means, threshold)
|
|
1903
2056
|
|
|
1904
2057
|
# ---- 3.5) 自驱打磨:全局评审不过 → 自动重改最弱章并重评(至多 2 轮,无需人工)
|
|
1905
2058
|
polish_rounds = 0
|
|
1906
|
-
while (not global_pass) and polish_rounds < 2 and chapter_scores:
|
|
2059
|
+
while (not global_pass) and polish_rounds < 2 and chapter_scores and global_means:
|
|
1907
2060
|
polish_rounds += 1
|
|
1908
2061
|
weak = _weakest_chapters(chapter_scores, global_means, threshold, limit=2)
|
|
1909
2062
|
if not weak:
|
|
@@ -1953,6 +2106,10 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
1953
2106
|
cj = {"scores": {}, "issues": [], "summary": "评审输出无法解析"}
|
|
1954
2107
|
cj_by_agent[agent["id"]] = cj
|
|
1955
2108
|
_check_cancel(ev)
|
|
2109
|
+
# 重评全挂(本轮所有评审都解析不出分数)→ 保留该章原分与达标态:
|
|
2110
|
+
# 拿「无法重评」覆盖真实分数,会把打磨前的好章误标 0 分、误判不达标
|
|
2111
|
+
repolished = any(isinstance(c.get("scores"), dict) and c["scores"]
|
|
2112
|
+
for c in cj_by_agent.values())
|
|
1956
2113
|
vals = {}
|
|
1957
2114
|
for d in dims:
|
|
1958
2115
|
xs = [float(cj["scores"].get(d, 0)) for cj in cj_by_agent.values()
|
|
@@ -1960,38 +2117,22 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
1960
2117
|
vals[d] = round(sum(xs) / len(xs), 1) if xs else 0.0
|
|
1961
2118
|
for c2 in chapter_scores:
|
|
1962
2119
|
if c2["chapter"] == i:
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
2120
|
+
if repolished:
|
|
2121
|
+
c2["means"] = vals
|
|
2122
|
+
c2["passed"] = bool(vals) and all(v >= threshold_ch for v in vals.values())
|
|
2123
|
+
c2["rounds"] = int(c2.get("rounds") or 1) + 1
|
|
2124
|
+
c2["polished"] = True
|
|
1967
2125
|
c2["words"] = _wc(_read_chapter(workdir, i))
|
|
1968
2126
|
fixed.append(i)
|
|
1969
2127
|
store.update_run(run_id, chapter_scores=chapter_scores)
|
|
1970
2128
|
_check_cancel(ev)
|
|
1971
|
-
#
|
|
2129
|
+
# 重评全书一致性(同一评审闭包;本轮全挂则保留上一轮结论——评审链挂了
|
|
2130
|
+
# 不代表书变差,不能拿「无法评审」覆盖真实分数)
|
|
1972
2131
|
full_text = "\n\n".join(_read_chapter(workdir, i2) for i2 in range(1, end + 1))
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
"summary": "(mock)打磨后全书达标"}
|
|
1978
|
-
else:
|
|
1979
|
-
res3 = _run_step(run_id, "global-critique", modelhub.bind_agent(agent, difficulty),
|
|
1980
|
-
(SERIAL_GLOBAL_PROMPT.replace("__DIMKEYS__", dimkey)
|
|
1981
|
-
.replace("__GOAL__", task["goal"])
|
|
1982
|
-
.replace("__MANUSCRIPT__", full_text[:60000])),
|
|
1983
|
-
workdir, readonly=True, ev=ev, timeout=2400)
|
|
1984
|
-
gj2 = runner.extract_json(res3.get("text") or "")
|
|
1985
|
-
if not isinstance(gj2, dict) or not isinstance(gj2.get("scores"), dict):
|
|
1986
|
-
gj2 = {"scores": {}, "issues": [], "summary": "全局评审输出无法解析"}
|
|
1987
|
-
global_issues.extend({"chapter": "全书", **it} for it in (gj2.get("issues") or [])[:8])
|
|
1988
|
-
for d in dims:
|
|
1989
|
-
v = gj2.get("scores", {}).get(d)
|
|
1990
|
-
if v is not None:
|
|
1991
|
-
gmeans.setdefault(d, []).append(float(v))
|
|
1992
|
-
_check_cancel(ev)
|
|
1993
|
-
global_means = {d: round(sum(xs) / len(xs), 1) for d, xs in gmeans.items()}
|
|
1994
|
-
global_pass = _all_ge(global_means, threshold)
|
|
2132
|
+
gscored2, gmeans_acc2 = run_global_round(critics)
|
|
2133
|
+
if gscored2:
|
|
2134
|
+
global_means = {d: round(sum(xs) / len(xs), 1) for d, xs in gmeans_acc2.items()}
|
|
2135
|
+
global_pass = _all_ge(global_means, threshold)
|
|
1995
2136
|
store.finish_step(run_id, pstep["n"], "done" if global_pass else "failed",
|
|
1996
2137
|
summary="重改 %s;打磨后全局 %s(%s)" % (
|
|
1997
2138
|
"、".join("第 %d 章" % x for x in fixed),
|
|
@@ -2064,6 +2205,120 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2064
2205
|
ended_at=_now())
|
|
2065
2206
|
|
|
2066
2207
|
|
|
2208
|
+
# ---- Best-of-N 赛马起草(非连载单稿;借鉴 freebuff editor-multi-prompt + best-of-n-selector)----
|
|
2209
|
+
# 连载路径的同章赛马是另一套(_run_serial_review 的 n_variants 分支,逐变体全维度
|
|
2210
|
+
# 评分),互不影响。赛马与压缩会话互斥(同连载赛马的守卫):并行 _run_step 会在
|
|
2211
|
+
# 同一 run 的会话事件流里交错,压缩面选择区域可能被搅乱。
|
|
2212
|
+
|
|
2213
|
+
# 每路候选的写法策略(刻意制造多样性,不是同提示词跑 N 遍);0 号不带策略=现状等价
|
|
2214
|
+
BESTOF_STRATEGIES = (
|
|
2215
|
+
"",
|
|
2216
|
+
"\n\n## 写法策略\n本稿以「场景与画面感」优先:多用具体可感的细节、动作与环境推进,少直接概括。",
|
|
2217
|
+
"\n\n## 写法策略\n本稿以「对话与冲突」驱动:让人物在对话与碰撞中推进情节,节奏明快、信息密度高。",
|
|
2218
|
+
)
|
|
2219
|
+
|
|
2220
|
+
BESTOF_SELECTOR_PROMPT = """你是终审编辑。同一个写作任务并行产生了多份候选稿,请对比选出最好的一份。
|
|
2221
|
+
|
|
2222
|
+
## 任务目标
|
|
2223
|
+
__GOAL__
|
|
2224
|
+
|
|
2225
|
+
## 对比要点(按重要性)
|
|
2226
|
+
贴合任务要求与评审维度 > 结构与可读性 > 语言质量 > 不跑题、不注水
|
|
2227
|
+
|
|
2228
|
+
__CANDIDATES__
|
|
2229
|
+
|
|
2230
|
+
## 输出
|
|
2231
|
+
只输出一个 JSON 对象,不要输出任何其他内容:
|
|
2232
|
+
{"pick": <选中候选的编号(从 0 开始的整数)>, "reason": "<一句话理由>", "improvements": "<落选稿里值得吸收进选中稿的具体优点,多条用分号隔开;没有就给空字符串>"}
|
|
2233
|
+
"""
|
|
2234
|
+
|
|
2235
|
+
|
|
2236
|
+
def _variant_name(ms_name, k):
|
|
2237
|
+
"""候选稿文件名:manuscript.md → manuscript.v0.md(无扩展名则尾部追加)。"""
|
|
2238
|
+
m = re.search(r"(\.[^./\\]+)$", ms_name)
|
|
2239
|
+
return (ms_name[:m.start()] + ".v%d" % k + m.group(1)) if m else (ms_name + ".v%d" % k)
|
|
2240
|
+
|
|
2241
|
+
|
|
2242
|
+
def _bestof_draft(run_id, task, impl, prompt_fn, ms_name, workdir, step_wd, ev,
|
|
2243
|
+
resume_ctx, difficulty, best_of, sel_agent, write_ms, note=""):
|
|
2244
|
+
"""非连载评审流的 Best-of-N 赛马起草。
|
|
2245
|
+
|
|
2246
|
+
N 路并行起草到各自变体文件(每路带不同写法策略)→ 终审选择器单次调用对比
|
|
2247
|
+
择优并回收落选稿精华 → 胜者写回正式稿名。某路失败只弃那路;全败返回其中
|
|
2248
|
+
一路的原始结果(保持原报错行为);选择器失败/不可解析回落 0 号候选(等价
|
|
2249
|
+
单稿行为)。选择结果与败者精华记入 run.bestof,变体文件保留供用户比对。"""
|
|
2250
|
+
n = max(2, min(3, int(best_of)))
|
|
2251
|
+
results = {}
|
|
2252
|
+
|
|
2253
|
+
def _one(kk):
|
|
2254
|
+
vfile = _variant_name(ms_name, kk)
|
|
2255
|
+
p = prompt_fn(vfile) + BESTOF_STRATEGIES[kk % len(BESTOF_STRATEGIES)]
|
|
2256
|
+
r = _run_step(run_id, "draft-v%d" % kk,
|
|
2257
|
+
modelhub.bind_agent(impl, difficulty), p, step_wd,
|
|
2258
|
+
readonly=False, ev=ev,
|
|
2259
|
+
# 只有 0 号候选继承续会话(N 路共用同一 CLI 会话会互相践踏)
|
|
2260
|
+
resume=(resume_ctx["session"] if (resume_ctx and kk == 0) else None),
|
|
2261
|
+
images=_task_images(task, workdir),
|
|
2262
|
+
note=(note + " · " if note else "") + "候选 %d/%d" % (kk + 1, n))
|
|
2263
|
+
txt = ""
|
|
2264
|
+
try:
|
|
2265
|
+
txt = _read_text_any_enc(os.path.join(workdir, vfile))
|
|
2266
|
+
except Exception:
|
|
2267
|
+
txt = ""
|
|
2268
|
+
results[kk] = (vfile, r, (txt or "").strip())
|
|
2269
|
+
|
|
2270
|
+
threads = []
|
|
2271
|
+
for kk in range(n):
|
|
2272
|
+
th = threading.Thread(target=_one, args=(kk,),
|
|
2273
|
+
name="bestof-%s-v%d" % (run_id, kk), daemon=True)
|
|
2274
|
+
threads.append(th)
|
|
2275
|
+
th.start()
|
|
2276
|
+
for th in threads:
|
|
2277
|
+
th.join(3000)
|
|
2278
|
+
_check_cancel(ev)
|
|
2279
|
+
|
|
2280
|
+
candidates = []
|
|
2281
|
+
for kk in range(n):
|
|
2282
|
+
vfile, r, txt = results.get(kk, (None, None, ""))
|
|
2283
|
+
if r is not None and r.get("ok") and txt:
|
|
2284
|
+
candidates.append({"k": kk, "file": vfile, "res": r, "text": txt})
|
|
2285
|
+
if not candidates:
|
|
2286
|
+
r = (results.get(0) or results.get(n - 1) or (None, None, ""))[1]
|
|
2287
|
+
return (r or {"ok": False, "error": "全部候选起草失败"}), None
|
|
2288
|
+
|
|
2289
|
+
# 终审选择器:单次调用对比全部候选(结构化输出 + 败者精华回收)
|
|
2290
|
+
cand_blocks = "\n\n".join(
|
|
2291
|
+
"### 候选 %d\n\n%s" % (c["k"], c["text"]) for c in candidates)
|
|
2292
|
+
sel_prompt = (BESTOF_SELECTOR_PROMPT
|
|
2293
|
+
.replace("__GOAL__", task["goal"])
|
|
2294
|
+
.replace("__CANDIDATES__", cand_blocks))
|
|
2295
|
+
pick, reason, improvements = candidates[0]["k"], "", ""
|
|
2296
|
+
sel_res = _run_step(run_id, "bestof-select",
|
|
2297
|
+
modelhub.bind_agent(sel_agent, difficulty),
|
|
2298
|
+
sel_prompt, workdir, readonly=True, ev=ev, note="候选择优")
|
|
2299
|
+
cj = runner.extract_json(sel_res.get("text") or "") if sel_res.get("ok") else None
|
|
2300
|
+
if isinstance(cj, dict):
|
|
2301
|
+
try:
|
|
2302
|
+
pk = int(cj.get("pick"))
|
|
2303
|
+
except Exception:
|
|
2304
|
+
pk = -1
|
|
2305
|
+
if any(c["k"] == pk for c in candidates):
|
|
2306
|
+
pick = pk
|
|
2307
|
+
reason = str(cj.get("reason") or "")[:300]
|
|
2308
|
+
improvements = str(cj.get("improvements") or "")[:600]
|
|
2309
|
+
winner = next(c for c in candidates if c["k"] == pick)
|
|
2310
|
+
winner["reason"] = reason
|
|
2311
|
+
winner["improvements"] = improvements
|
|
2312
|
+
write_ms(winner["text"])
|
|
2313
|
+
store.update_run(run_id, bestof={
|
|
2314
|
+
"pick": winner["k"], "file": winner["file"],
|
|
2315
|
+
"candidates": [c["k"] for c in candidates],
|
|
2316
|
+
"reason": reason, "improvements": improvements,
|
|
2317
|
+
"selector": (sel_agent.get("id") or "") if isinstance(sel_agent, dict) else "",
|
|
2318
|
+
})
|
|
2319
|
+
return winner["res"], winner
|
|
2320
|
+
|
|
2321
|
+
|
|
2067
2322
|
def _run_content_review(run, task, agents, ev, stats, mode):
|
|
2068
2323
|
run_id = run["id"]
|
|
2069
2324
|
workdir = task["workdir"]
|
|
@@ -2120,6 +2375,7 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
2120
2375
|
store.update_run(run_id, outline=outline)
|
|
2121
2376
|
|
|
2122
2377
|
# 1) 起草
|
|
2378
|
+
bestof_improvements = "" # 赛马败者精华(真实路径由选择器填充;mock 路径恒空)
|
|
2123
2379
|
if impl.get("mode") == "mock":
|
|
2124
2380
|
step, log_abs = store.add_step(run_id, "draft", impl["id"], impl.get("label"), note=draft_note)
|
|
2125
2381
|
write_ms(mocks.draft_manuscript(task, 1))
|
|
@@ -2131,16 +2387,32 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
2131
2387
|
except Exception:
|
|
2132
2388
|
pass
|
|
2133
2389
|
else:
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2390
|
+
def _draft_prompt_for(vfile):
|
|
2391
|
+
p = (_tpl(task, "draft_prompt", NOVEL_DRAFT_PROMPT).replace("__FILE__", vfile)
|
|
2392
|
+
.replace("__GOAL__", task["goal"])
|
|
2393
|
+
.replace("__CONTEXT__", task.get("context") or "(无)"))
|
|
2394
|
+
if outline:
|
|
2395
|
+
p += "\n\n## 编排者大纲(按要点组织稿件)\n" + \
|
|
2396
|
+
"\n".join("- " + i for i in outline["items"])
|
|
2397
|
+
return p
|
|
2398
|
+
|
|
2399
|
+
best_of = max(1, min(3, int(task.get("best_of") or 1)))
|
|
2400
|
+
if best_of >= 2 and not _compaction_enabled():
|
|
2401
|
+
try:
|
|
2402
|
+
sel_agent = critics[0]
|
|
2403
|
+
except Exception:
|
|
2404
|
+
sel_agent = impl
|
|
2405
|
+
draft_res, _bw = _bestof_draft(
|
|
2406
|
+
run_id, task, impl, _draft_prompt_for, ms_name, workdir, step_wd,
|
|
2407
|
+
ev, resume_ctx, difficulty, best_of, sel_agent, write_ms,
|
|
2408
|
+
note=draft_note)
|
|
2409
|
+
bestof_improvements = (_bw or {}).get("improvements") or ""
|
|
2410
|
+
else:
|
|
2411
|
+
draft_res = _run_step(run_id, "draft", modelhub.bind_agent(impl, difficulty),
|
|
2412
|
+
_draft_prompt_for(ms_name), step_wd, readonly=False,
|
|
2413
|
+
ev=ev, note=draft_note,
|
|
2414
|
+
resume=resume_ctx["session"] if resume_ctx else None,
|
|
2415
|
+
images=_task_images(task, workdir))
|
|
2144
2416
|
if not draft_res["ok"]:
|
|
2145
2417
|
store.update_run(run_id, status="failed", error="起草失败: %s" % draft_res.get("error"),
|
|
2146
2418
|
ended_at=_now())
|
|
@@ -2199,6 +2471,11 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
2199
2471
|
majors = [i for i in issues_all if i.get("severity") == "major"][:8]
|
|
2200
2472
|
for i in majors:
|
|
2201
2473
|
crit_lines.append("- [%s] %s" % (i.get("dim", "?"), str(i.get("note", ""))[:120]))
|
|
2474
|
+
if r == 1 and bestof_improvements:
|
|
2475
|
+
# 败者精华回收(freebuff suggestedImprovements 的落地):终审从落选
|
|
2476
|
+
# 候选稿提炼的优点,首轮修订时与评审意见一并喂给作者
|
|
2477
|
+
crit_lines.append("- [赛马精华] 终审择优时从落选候选稿提炼出值得吸收的优点:%s"
|
|
2478
|
+
% bestof_improvements[:400])
|
|
2202
2479
|
if impl.get("mode") == "mock":
|
|
2203
2480
|
step, log_abs = store.add_step(run_id, "revise-r%d" % r, impl["id"], impl.get("label"))
|
|
2204
2481
|
write_ms(mocks.draft_manuscript(task, r + 1))
|
|
@@ -2268,6 +2545,54 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
2268
2545
|
|
|
2269
2546
|
# ---------------------------------------------------------------- 入口
|
|
2270
2547
|
|
|
2548
|
+
SERIAL_QA_PROMPT = """你是网文《__TITLE__》的责任编辑(不要修改任何文件)。读者(作者本人)就这本书提了一个问题,请直接回答:
|
|
2549
|
+
|
|
2550
|
+
__QUESTION__
|
|
2551
|
+
|
|
2552
|
+
回答要求:
|
|
2553
|
+
- 只回答问题,不要重写章节、不要改动任何文件。
|
|
2554
|
+
- 先给结论,再给依据;涉及章节时点明具体文件(如 chapter-09.md)。
|
|
2555
|
+
- 问题若暴露了稿件自身毛病(缺章、编号错位、前后矛盾),说清楚坏在哪个文件、该怎么修。
|
|
2556
|
+
|
|
2557
|
+
各章成稿都在当前工作目录(chapter-XX.md),合并稿 manuscript.md 可能只含部分章节,可按需查阅。"""
|
|
2558
|
+
|
|
2559
|
+
|
|
2560
|
+
def _run_serial_qa(run, task, agents, ev):
|
|
2561
|
+
"""连载答疑轮:追话提问不再整本重跑——单步只读问答,答案落步骤 output,
|
|
2562
|
+
对话页时间线(消息气泡 + 步骤正文)直接可读(2026-09-18「为啥没有第九章」
|
|
2563
|
+
案:一句追问被当成完整编排指令,重评 8 章还烧出连环自动续跑)。"""
|
|
2564
|
+
run_id = run["id"]
|
|
2565
|
+
workdir = run.get("workdir") or task.get("workdir") or ""
|
|
2566
|
+
question = (run.get("qa_text") or "").strip() or "(见下方读者追问)"
|
|
2567
|
+
prompt = (SERIAL_QA_PROMPT
|
|
2568
|
+
.replace("__TITLE__", (task.get("title") or "本书").lstrip("# ").strip())
|
|
2569
|
+
.replace("__QUESTION__", question))
|
|
2570
|
+
# 回答者顺序:评审组(专职读稿)→ 实现者 → 其余已启用真实智能体;死链自动落到下一个
|
|
2571
|
+
critic_ids = task.get("critics") or []
|
|
2572
|
+
impl_id = task.get("implementer") or ""
|
|
2573
|
+
ordered = [a for a in agents if a.get("id") in critic_ids]
|
|
2574
|
+
ordered += [a for a in agents
|
|
2575
|
+
if a.get("id") == impl_id and a.get("id") not in critic_ids]
|
|
2576
|
+
ordered += [a for a in agents
|
|
2577
|
+
if a.get("mode") == "real"
|
|
2578
|
+
and a.get("id") not in critic_ids and a.get("id") != impl_id]
|
|
2579
|
+
ordered = [a for a in ordered if a.get("mode") != "mock"] or ordered[:1]
|
|
2580
|
+
errors = []
|
|
2581
|
+
for agent in ordered[:3]:
|
|
2582
|
+
_check_cancel(ev)
|
|
2583
|
+
res = _run_step(run_id, "qa", modelhub.bind_agent(agent, "default"),
|
|
2584
|
+
prompt, workdir, readonly=True, ev=ev, timeout=1200)
|
|
2585
|
+
if res.get("ok") and (res.get("text") or "").strip():
|
|
2586
|
+
store.update_run(run_id, status="done", ended_at=_now(),
|
|
2587
|
+
verdict={"qa": True,
|
|
2588
|
+
"answered_by": agent.get("id")})
|
|
2589
|
+
return
|
|
2590
|
+
errors.append("%s:%s" % (agent.get("id"),
|
|
2591
|
+
(res.get("error") or "无输出")[:120]))
|
|
2592
|
+
store.update_run(run_id, status="failed", ended_at=_now(),
|
|
2593
|
+
error="答疑失败(执行/评审链不可用)——" + ";".join(errors[-3:]))
|
|
2594
|
+
|
|
2595
|
+
|
|
2271
2596
|
def execute_run(run_id):
|
|
2272
2597
|
run = store.get_run(run_id)
|
|
2273
2598
|
if not run:
|
|
@@ -2303,6 +2628,8 @@ def execute_run(run_id):
|
|
|
2303
2628
|
# 任务分支裁决状态:新一轮 run 产生新分支内容,重置回「待裁决」
|
|
2304
2629
|
store.set_task_git_state(task["id"], "isolated")
|
|
2305
2630
|
agents = _agents()
|
|
2631
|
+
global _CURRENT_AGENTS
|
|
2632
|
+
_CURRENT_AGENTS = agents
|
|
2306
2633
|
# 续会话是对该 CLI 的显式指定:目标未启用编排时也注入本次运行(不影响路由池)
|
|
2307
2634
|
want = ((task.get("resume") or {}).get("agent") or "").strip()
|
|
2308
2635
|
if want and _pick(agents, want) is None:
|
|
@@ -2316,6 +2643,11 @@ def execute_run(run_id):
|
|
|
2316
2643
|
# direct=单 CLI 直达(无拆解/评审,信箱续轮即对话)
|
|
2317
2644
|
engine = task.get("engine") or ("code" if task["type"] == "code" else "review")
|
|
2318
2645
|
try:
|
|
2646
|
+
# 连载答疑轮:op=qa 不走编排流水线,单步只读回答后即收尾;
|
|
2647
|
+
# 放进 try——Cancelled 与主流程同口径收口为 cancelled
|
|
2648
|
+
if run.get("op") == "qa":
|
|
2649
|
+
_run_serial_qa(run, task, agents, ev)
|
|
2650
|
+
return
|
|
2319
2651
|
if engine == "code":
|
|
2320
2652
|
_run_code(run, task, agents, ev, stats, mode)
|
|
2321
2653
|
elif engine == "direct":
|