codebee 0.1.5 → 0.1.7

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.
@@ -273,18 +273,45 @@ def _wait_gate(run_id, ev):
273
273
  time.sleep(1.0)
274
274
 
275
275
 
276
+ def _binding_dead_msg(agent):
277
+ """死链失败文案:委托 modelhub 单一真源(告警 sync 共用同一文案)。"""
278
+ try:
279
+ from . import modelhub
280
+ return modelhub.binding_dead_msg(agent.get("id") or "")
281
+ except Exception:
282
+ return ("绑定链全部失效,本步判失败、不回落 CLI 本机默认——"
283
+ "请在「CLI 绑定」页为该 CLI 绑定已启用的供应商")
284
+
285
+
276
286
  def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner.DEFAULT_TIMEOUT, note="", resume=None, images=None, require_tools=False):
277
287
  """执行一个智能体步骤并记录。返回 runner 统一结果。"""
278
288
  _wait_gate(run_id, ev)
279
- # 绑定解析为空 CLI 将回落本机默认配置(用户配置的模型/供应商全部不生效)。
280
- # 2026-09-16 实测:这种状态下烧干配额的本机默认供应商被静默使用,用户以为
281
- # 在用自己配的模型。首次出现时在步骤备注里醒目标出。
282
- if agent.get("mode") == "real" and not (agent.get("call_chain") or agent.get("env")):
283
- note = ((note + ";") if note else "") + \
284
- "⚠ 未解析到绑定链,本步回落 CLI 本机默认配置(请在模型接入页检查该 CLI 的供应商绑定)"
289
+ # 绑定解析为空分两种(2026-09-18 区分对待):
290
+ # · 从没配过链(binding_configured=False):回落 CLI 本机默认照跑——
291
+ # 用户根本没在 CodeBee 里配供应商,谈不到「烧自己配的配额」;判失败
292
+ # 反而把用本地登录的普通用户全挡在门外(0.1.6 真实装机误伤案例)。
293
+ # · 配过链但全死(binding_configured=True):本步判失败不静默降级——
294
+ # 宁可失败不偷跑本机默认;auto 流程实现步的既有换将会接手健康 CLI
295
+ # 只记在步骤备注/错误里,不再产生全局健康告警胶囊(同上案例:红胶囊
296
+ # 吓不到也帮不到普通用户,2026-09-18 用户拍板移除)。
297
+ dead_binding = (agent.get("mode") == "real"
298
+ and agent.get("binding_configured")
299
+ and not (agent.get("call_chain") or agent.get("env")))
300
+ dead_msg = _binding_dead_msg(agent) if dead_binding else ""
301
+ if dead_binding:
302
+ note = ((note + ";") if note else "") + "⚠ " + dead_msg
285
303
  step, log_abs = store.add_step(run_id, role, agent["id"],
286
304
  agent.get("label", agent["id"]), note=note)
287
305
  start = time.time()
306
+ if dead_binding:
307
+ from .error_codes import ErrorCode
308
+ res = {"ok": False, "text": "", "json": None, "cost_usd": 0.0,
309
+ "tokens": 0, "usage": None, "error": dead_msg,
310
+ "error_code": ErrorCode.ENV_BLOCK,
311
+ "raw": {"exit_code": None}, "kind": agent.get("kind", "generic"),
312
+ "model": agent.get("model")}
313
+ _finish_step_result(run_id, step, res, role, agent, start)
314
+ return res
288
315
  if agent.get("mode") == "mock":
289
316
  time.sleep(0.3)
290
317
  res = {"ok": True, "text": "[mock] %s" % prompt[:80], "json": None,
@@ -1601,12 +1628,19 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
1601
1628
  res = None
1602
1629
  good = False
1603
1630
  txt = ""
1631
+ use_prompt = prompt
1604
1632
  for draft_attempt in range(3):
1605
1633
  if draft_attempt:
1606
1634
  if ev is not None and ev.is_set():
1607
1635
  break
1608
1636
  time.sleep(30 * draft_attempt) # 30s / 60s 退避
1609
- res = _run_step(run_id, "draft-c%d" % i, modelhub.bind_agent(impl, difficulty), prompt,
1637
+ if draft_attempt and len(prompt) > 12000 and sk_block and sk_block in prompt:
1638
+ # 长提示词在容量受限通道(讯飞托管 35B 等)上会挂起/秒拒
1639
+ # ——降级重试:经验库块截到 4K 字,保留大纲/前情/本章要点
1640
+ # (2026-09-17 七猫实测:全量 30KB 对讯飞必挂)
1641
+ use_prompt = prompt.replace(
1642
+ sk_block, sk_block[:4000] + "\n\n(经验库已因通道容量限制精简)")
1643
+ res = _run_step(run_id, "draft-c%d" % i, modelhub.bind_agent(impl, difficulty), use_prompt,
1610
1644
  step_wd, readonly=False, ev=ev, timeout=2400,
1611
1645
  resume=resume_ctx["session"] if resume_ctx else None,
1612
1646
  images=_task_images(task, workdir),
@@ -1626,15 +1660,23 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
1626
1660
  break
1627
1661
  if ev is not None and ev.is_set():
1628
1662
  break
1629
- # 同作者重试穷尽 → 起草换将(连载不断档优先,风格差异交评审门与
1630
- # 后续 revise 拉回)——与代码流程换将同款语义(2026-09-17 c34 实测)
1663
+ # 同作者重试穷尽 → 起草换将:按路由分序逐个试备选(最多 2 个,
1664
+ # 只试一个会让第二名没机会——2026-09-17 c35 实测 opencode 顶在
1665
+ # 前面,能干活的 kimi 永远轮不上)。连载不断档优先,风格差异交
1666
+ # 评审门与后续 revise 拉回。
1631
1667
  if not good:
1632
- other, other_reason = router.pick(
1633
- agents, "implement", task.get("type") or "serial", None,
1634
- exclude={impl["id"], "mock-a", "mock-b"})
1635
- if other and other.get("mode") == "real":
1668
+ tried = {impl["id"], "mock-a", "mock-b"}
1669
+ for _alt in range(2):
1670
+ if good or (ev is not None and ev.is_set()):
1671
+ break
1672
+ other, other_reason = router.pick(
1673
+ agents, "implement", task.get("type") or "serial", None,
1674
+ exclude=tried)
1675
+ if not (other and other.get("mode") == "real"):
1676
+ break
1677
+ tried.add(other["id"])
1636
1678
  res = _run_step(run_id, "draft-c%d" % i,
1637
- modelhub.bind_agent(other, difficulty), prompt,
1679
+ modelhub.bind_agent(other, difficulty), use_prompt,
1638
1680
  step_wd, readonly=False, ev=ev, timeout=2400,
1639
1681
  images=_task_images(task, workdir),
1640
1682
  note="起草换将 %s → %s:%s" % (
@@ -1649,6 +1691,9 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
1649
1691
  pass
1650
1692
  if good:
1651
1693
  draft_sid = "" # 换将作者无本任会话,revise 另起
1694
+ if not good:
1695
+ time.sleep(3) # 落盘竞态宽限:CLI 崩溃退出前写的文件可能晚于
1696
+ good, txt = _chapter_state() # 退出检查零点几秒才可见(c34 实测)
1652
1697
  if not good:
1653
1698
  store.update_run(run_id, status="failed",
1654
1699
  error="第 %d 章起草失败: %s" % (i, (res or {}).get("error")), ended_at=_now())
@@ -1668,53 +1713,68 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
1668
1713
  # 均分最高者为正稿。变体写隔离文件 chapter-XX-vK.md,赢家改名、
1669
1714
  # 败稿删除;变体 0 = 本任作者(revise 会话沿用),其余取跨族优先的
1670
1715
  # 其他真实智能体,不足时同作者开新会话凑数。
1671
- pool = [impl]
1672
- others = [a for a in agents if a.get("mode") == "real" and a["id"] != impl["id"]]
1673
- others.sort(key=lambda a: 0 if a.get("kind") != impl.get("kind") else 1)
1674
- pool += others[:n_variants - 1]
1675
- while len(pool) < n_variants:
1676
- pool.append(impl) # 不够就同作者再开一路(新会话天然出不同稿)
1677
- results = {}
1678
-
1679
- def _draft_one(kk, agent):
1680
- vfile = "chapter-%02d-v%d.md" % (i, kk)
1681
- r = _run_step(run_id, "draft-c%d-v%d" % (i, kk),
1682
- modelhub.bind_agent(agent, difficulty),
1683
- _draft_prompt(vfile), step_wd, readonly=False, ev=ev,
1684
- timeout=2400,
1685
- # 赛马只在全新起草时启用(无续会话),每路都是新会话
1686
- images=_task_images(task, workdir),
1687
- note="赛马变体 %d/%d(%s)" % (kk + 1, len(pool), agent.get("id")))
1688
- results[kk] = (vfile, agent, r)
1689
-
1690
- threads = []
1691
- for kk, agent in enumerate(pool):
1692
- th = threading.Thread(target=_draft_one, args=(kk, agent),
1693
- name="race-%s-c%d-v%d" % (run_id, i, kk), daemon=True)
1694
- threads.append(th)
1695
- th.start()
1696
- for th in threads:
1697
- th.join(3000)
1698
- _check_cancel(ev)
1699
-
1700
1716
  scored_variants = []
1701
- for kk in range(len(pool)):
1702
- vfile, agent, r = results.get(kk, (None, None, None))
1703
- if vfile is None:
1704
- continue
1705
- txt = _read_variant(workdir, i, kk)
1706
- ok_text = txt and _wc(txt) >= int(wpc * 0.6)
1707
- if r is not None and not r["ok"] and not ok_text:
1708
- continue # 这一路彻底失败(无成品也不够长)
1709
- if not ok_text:
1710
- continue
1711
- cj_map, sc, sids2 = run_critique(
1712
- txt, 1, note_extra="(本稿为同章赛马变体 %d/%d,只评这一份)" % (kk + 1, len(pool)))
1713
- m = means_of(cj_map)
1714
- avg = round(sum(m.values()) / max(1, len(m)), 2) if m else 0.0
1715
- scored_variants.append({"variant": kk, "agent": agent.get("id"),
1716
- "file": vfile, "means": m, "avg": avg,
1717
- "cj": cj_map, "scored": sc, "sids": sids2})
1717
+ for race_round in range(2):
1718
+ # 全变体失败(网关突发限流)→ 60s 退避重赛一轮,别一章判死
1719
+ if race_round:
1720
+ if ev is not None and ev.is_set():
1721
+ break
1722
+ time.sleep(60)
1723
+ for kk in range(n_variants):
1724
+ # 清上一轮残稿:防陈旧半成品被本轮评分误认成新成品
1725
+ try:
1726
+ os.remove(os.path.join(workdir, "chapter-%02d-v%d.md" % (i, kk)))
1727
+ except OSError:
1728
+ pass
1729
+ pool = [impl]
1730
+ others = [a for a in agents if a.get("mode") == "real" and a["id"] != impl["id"]]
1731
+ others.sort(key=lambda a: 0 if a.get("kind") != impl.get("kind") else 1)
1732
+ pool += others[:n_variants - 1]
1733
+ while len(pool) < n_variants:
1734
+ pool.append(impl) # 不够就同作者再开一路(新会话天然出不同稿)
1735
+ results = {}
1736
+
1737
+ def _draft_one(kk, agent):
1738
+ vfile = "chapter-%02d-v%d.md" % (i, kk)
1739
+ r = _run_step(run_id, "draft-c%d-v%d" % (i, kk),
1740
+ modelhub.bind_agent(agent, difficulty),
1741
+ _draft_prompt(vfile), step_wd, readonly=False, ev=ev,
1742
+ timeout=2400,
1743
+ # 赛马只在全新起草时启用(无续会话),每路都是新会话
1744
+ images=_task_images(task, workdir),
1745
+ note="赛马变体 %d/%d(%s)" % (kk + 1, len(pool), agent.get("id")))
1746
+ results[kk] = (vfile, agent, r)
1747
+
1748
+ threads = []
1749
+ for kk, agent in enumerate(pool):
1750
+ th = threading.Thread(target=_draft_one, args=(kk, agent),
1751
+ name="race-%s-c%d-v%d" % (run_id, i, kk), daemon=True)
1752
+ threads.append(th)
1753
+ th.start()
1754
+ for th in threads:
1755
+ th.join(3000)
1756
+ _check_cancel(ev)
1757
+
1758
+ scored_variants = []
1759
+ for kk in range(len(pool)):
1760
+ vfile, agent, r = results.get(kk, (None, None, None))
1761
+ if vfile is None:
1762
+ continue
1763
+ txt = _read_variant(workdir, i, kk)
1764
+ ok_text = txt and _wc(txt) >= int(wpc * 0.6)
1765
+ if r is not None and not r["ok"] and not ok_text:
1766
+ continue # 这一路彻底失败(无成品也不够长)
1767
+ if not ok_text:
1768
+ continue
1769
+ cj_map, sc, sids2 = run_critique(
1770
+ txt, 1, note_extra="(本稿为同章赛马变体 %d/%d,只评这一份)" % (kk + 1, len(pool)))
1771
+ m = means_of(cj_map)
1772
+ avg = round(sum(m.values()) / max(1, len(m)), 2) if m else 0.0
1773
+ scored_variants.append({"variant": kk, "agent": agent.get("id"),
1774
+ "file": vfile, "means": m, "avg": avg,
1775
+ "cj": cj_map, "scored": sc, "sids": sids2})
1776
+ if scored_variants:
1777
+ break
1718
1778
  if not scored_variants:
1719
1779
  store.update_run(run_id, status="failed",
1720
1780
  error="第 %d 章赛马全部变体起草失败" % i, ended_at=_now())
@@ -66,6 +66,10 @@ def _build_agent(entry):
66
66
  # 小时级 token 配额(可选,0/缺省=不限):路由时对本小时用量超标的
67
67
  # 智能体降权(munder-difflin 式配额感知),订阅型 CLI 不至于被单任务打爆
68
68
  "quota_tokens_per_hour": int(orch.get("quota_tokens_per_hour") or 0),
69
+ # 整包透传 orch:per-agent 运行时开关(timeout_ms 5E、stall_timeout_s
70
+ # 看门狗等)都在 runner 侧读取——此前只挑字段,catalog 上配的
71
+ # timeout_ms/stall 根本流不到 runner(2026-09-17 连载 c35 实测)
72
+ "orch": orch,
69
73
  }
70
74
 
71
75