codebee 0.1.3 → 0.1.4

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.
@@ -20,6 +20,7 @@ import threading
20
20
  import time
21
21
 
22
22
  from . import catalog, history, jobs, manager, modelhub, mocks, planner, registry, router, runner, skills, store, usage
23
+ from . import builtin_agent
23
24
  from . import diagnostics
24
25
  from . import paths as paths_mod
25
26
  from . import session_log as session_log_mod
@@ -325,6 +326,55 @@ def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner
325
326
  return res
326
327
 
327
328
 
329
+ def _run_builtin_step(run_id, role, bi, prompt, workdir, ev, note=""):
330
+ """内置智能体步骤:直连模型 API + 工具循环(builtin_agent),不经 CLI 进程。
331
+
332
+ 与 _run_step 对齐的三件事:暂停/取消闸门、运行中指令 drain 注入、重复调用
333
+ 守门;结果同样经 _finish_step_result 落步骤(output=干净回答)并入用量台账。
334
+ 日志只有「迭代/工具」摘要行——对话视图吃 output,日志抽屉看工具轨迹。"""
335
+ _wait_gate(run_id, ev)
336
+ step, log_abs = store.add_step(run_id, role, "builtin", "内置智能体", note=note)
337
+ start = time.time()
338
+ agent_pseudo = {"id": "builtin", "label": "内置智能体", "kind": "builtin", "mode": "real",
339
+ "provider": {"id": bi.get("provider_id") or "",
340
+ "name": bi.get("provider_name") or ""}}
341
+ guard = repeat_guard.check(run_id, role, prompt)
342
+ if guard["should_stop"]:
343
+ from .error_codes import ErrorCode
344
+ res = {"ok": False, "text": "", "usage": None, "cost_usd": 0.0, "tokens": 0,
345
+ "error": guard["reminder"], "error_code": ErrorCode.ENV_BLOCK,
346
+ "raw": {"exit_code": None}, "model": bi.get("model")}
347
+ _finish_step_result(run_id, step, res, role, agent_pseudo, start)
348
+ return res
349
+ # 运行中指挥:drain 用户追加的指令/附件,注入本轮(与 _run_step 同语义)
350
+ directive_block, _imgs = _drain_directives(run_id, workdir, role=role, step_n=step["n"])
351
+ if directive_block:
352
+ prompt = directive_block + "\n\n---\n\n" + prompt
353
+ if guard["reminder"]:
354
+ prompt = guard["reminder"] + "\n\n---\n\n" + prompt
355
+ lines = ["内置智能体(%s · %s)" % (bi.get("provider_name"), bi.get("model"))]
356
+
357
+ def _log(line):
358
+ lines.append(str(line))
359
+
360
+ res = builtin_agent.run(bi, prompt, workdir, cancel_event=ev, log=_log)
361
+ if log_abs:
362
+ try:
363
+ log_abs.write_text("\n".join(lines) + "\n", encoding="utf-8")
364
+ except Exception:
365
+ pass
366
+ usage = res.get("usage") or {}
367
+ res.setdefault("raw", {})
368
+ res["raw"]["exit_code"] = 0 if res.get("ok") else 1
369
+ res["raw"]["duration"] = time.time() - start
370
+ res["tokens"] = int(usage.get("total") or 0)
371
+ res.setdefault("cost_usd", 0.0)
372
+ # 先收尾再查取消:与 _run_step 同序,防步骤记录停在「运行中」变僵尸
373
+ _finish_step_result(run_id, step, res, role, agent_pseudo, start)
374
+ _check_cancel(ev)
375
+ return res
376
+
377
+
328
378
  def _budget_max_tokens():
329
379
  """§07 T2.1:单次 run 的 token 预算上限;0/未配置 = 不限。
330
380
 
@@ -851,6 +901,28 @@ DIRECT_DONE: <一句话说明本轮做了什么>
851
901
 
852
902
  DIRECT_MAX_TURNS = 200 # 对话续轮上限(每轮都要用户主动发消息才触发,防意外打满)
853
903
 
904
+ # 内置智能体版:人格与工具说明在 builtin_agent._SYSTEM_PROMPT,这里只给任务输入;
905
+ # 不要求 DIRECT_DONE 协议尾行——builtin 的最终回答本身就是干净文本
906
+ BUILTIN_DIRECT_PROMPT = """## 任务
907
+ __GOAL__
908
+
909
+ ## 背景与上下文
910
+ __CONTEXT__
911
+
912
+ ## 要求
913
+ - 能改直接改、能写直接写(用工具,限本工作目录内),产出文件一律 UTF-8 编码。
914
+ - 完成后直接给用户一段简短说明:做了什么、产出/修改了哪些文件。"""
915
+
916
+ BUILTIN_FOLLOWUP_PROMPT = """## 原始任务
917
+ __GOAL__
918
+
919
+ ## 上一轮输出(结尾)
920
+ __PREV__
921
+
922
+ ## 要求
923
+ - 优先回应用户的新消息(继续做/改/答疑均可),仍限本工作目录内,工具可用。
924
+ - 回复直接说清本轮做了什么、答案是什么。"""
925
+
854
926
 
855
927
  def _pending_messages(run_id):
856
928
  """该 run 信箱里未消费消息列表(读不到时当空,绝不因信箱异常打断执行)。"""
@@ -885,25 +957,36 @@ def _direct_last_text(run):
885
957
 
886
958
 
887
959
  def _run_direct(run, task, agents, ev, stats, mode):
888
- """直连引擎:目标+附件直接交给一个 CLI,跑完即止。
889
-
890
- 无规划/评审/验证/换将——快档位,质量交给执行 CLI 自身。对话式续轮:
891
- 运行中信箱来消息 drain 注入下一步(_run_step 既有机制);步骤结束后
892
- 信箱还有未消费消息就再续一轮(会话续接,同一 CLI 连续对话);信箱空了
893
- 收工为 done。运行结束后再来消息走 retry_task(消息自动继承到新 run)。
960
+ """直连引擎:目标+附件直接交给一个执行者,跑完即止。
961
+
962
+ 执行者优先级:内置智能体(直连模型 API + 工具循环,无 CLI 进程)→ CLI 智能体。
963
+ 任务显式声明 CLI 会话续接(resume)或手动指定了执行者时尊重选择走 CLI;
964
+ 无可用供应商时回退 CLI。无规划/评审/验证/换将——快档位。对话式续轮:
965
+ 运行中信箱来消息 drain 注入下一步;步骤结束后信箱还有未消费消息就
966
+ 再续一轮;信箱空了收工为 done。运行结束后再来消息走 retry_task
967
+ (消息自动继承到新 run)。
894
968
  """
895
969
  run_id = run["id"]
896
970
  workdir = task["workdir"]
897
971
  route = {}
898
972
  resume_ctx = _valid_resume(task, agents)
899
- if resume_ctx is not None:
973
+ bi = None
974
+ if resume_ctx is None and not (mode == "manual" and task.get("implementer")):
975
+ try:
976
+ bi = builtin_agent.resolve()
977
+ except Exception:
978
+ bi = None
979
+ if bi is not None:
980
+ impl = None
981
+ route["implementer"] = "内置智能体(%s · %s)" % (bi["provider_name"], bi["model"])
982
+ elif resume_ctx is not None:
900
983
  impl = resume_ctx["agent"]
901
984
  route["implementer"] = resume_ctx["note"]
902
985
  elif mode == "manual":
903
986
  impl, _ = _pick_implementer(agents, task.get("implementer"))
904
987
  else:
905
988
  impl, route["implementer"] = router.pick(agents, "implement", task["type"], stats)
906
- if impl is None:
989
+ if impl is None and bi is None:
907
990
  store.update_run(run_id, status="failed", error="没有可用智能体", ended_at=_now())
908
991
  return
909
992
  difficulty = task.get("difficulty") or "default"
@@ -913,8 +996,8 @@ def _run_direct(run, task, agents, ev, stats, mode):
913
996
  sid = (resume_ctx["session"] if resume_ctx else "") or ""
914
997
  last_text = ""
915
998
  # 追话起跑(/api/runs/<id>/chat → retry_task):信箱已有未消费消息 = 这是对话
916
- # 的下一轮而非首轮。继承上一轮的 CLI 会话 id,让它真的「接着上次聊」;
917
- # 同时把首步切成续轮档(DIRECT_FOLLOWUP),避免又走一遍开场的完整任务框架。
999
+ # 的下一轮而非首轮。CLI 继承上一轮的会话 id(真的「接着上次聊」),内置智能体
1000
+ # 靠「上一轮输出(结尾)」块带上下文;同时把首步切成续轮档。
918
1001
  try:
919
1002
  pending0 = store.peek_messages(run_id)
920
1003
  except Exception:
@@ -923,7 +1006,7 @@ def _run_direct(run, task, agents, ev, stats, mode):
923
1006
  prev = _direct_prev_run(task["id"], run_id)
924
1007
  if prev:
925
1008
  ps = (prev.get("direct_session") or {})
926
- if ps.get("agent") == impl["id"] and ps.get("session"):
1009
+ if impl is not None and ps.get("agent") == impl["id"] and ps.get("session"):
927
1010
  sid = sid or ps["session"]
928
1011
  if ps.get("workdir"):
929
1012
  step_wd = ps["workdir"]
@@ -933,38 +1016,55 @@ def _run_direct(run, task, agents, ev, stats, mode):
933
1016
  while True:
934
1017
  _wait_gate(run_id, ev)
935
1018
  if first:
936
- prompt = (DIRECT_PROMPT
937
- .replace("__GOAL__", task["goal"])
938
- .replace("__CONTEXT__", task.get("context") or "(无)"))
1019
+ if bi is not None:
1020
+ prompt = (BUILTIN_DIRECT_PROMPT
1021
+ .replace("__GOAL__", task["goal"])
1022
+ .replace("__CONTEXT__", task.get("context") or "(无)"))
1023
+ else:
1024
+ prompt = (DIRECT_PROMPT
1025
+ .replace("__GOAL__", task["goal"])
1026
+ .replace("__CONTEXT__", task.get("context") or "(无)"))
939
1027
  note = route.get("implementer", "")
940
1028
  images = _task_images(task, workdir)
941
1029
  else:
942
- prompt = DIRECT_FOLLOWUP_PROMPT.replace("__GOAL__", task["goal"])
943
- if not sid and last_text:
944
- # 无会话续接能力的 CLI(如 dsh 一次性任务):把上一轮输出尾部带进上下文
945
- prompt += "\n\n## 上一轮输出(结尾)\n" + last_text[-3000:]
1030
+ if bi is not None:
1031
+ prompt = (BUILTIN_FOLLOWUP_PROMPT
1032
+ .replace("__GOAL__", task["goal"])
1033
+ .replace("__PREV__", (last_text or "(无)")[-3000:]))
1034
+ else:
1035
+ prompt = DIRECT_FOLLOWUP_PROMPT.replace("__GOAL__", task["goal"])
1036
+ if not sid and last_text:
1037
+ # 无会话续接能力的 CLI(如 dsh 一次性任务):把上一轮输出尾部带进上下文
1038
+ prompt += "\n\n## 上一轮输出(结尾)\n" + last_text[-3000:]
946
1039
  note = "对话续轮"
947
1040
  images = None
948
1041
  # 续轮判据:只有「本步执行期间新到」的消息才再开一轮。
949
- # 不能只看「信箱非空」——真实步骤的 drain 在 _run_step 内部发生,起跑前
950
- # 就积压的消息会被本步吃掉(peek 归零);而 mock/不走 drain 的路径消息
951
- # 永远不消费,只看非空会空转到轮数上限。比较步骤前后的未消费数即可区分。
1042
+ # 不能只看「信箱非空」——真实步骤的 drain 在 _run_step/_run_builtin_step
1043
+ # 内部发生,起跑前就积压的消息会被本步吃掉(peek 归零);而 mock/不走
1044
+ # drain 的路径消息永远不消费,只看非空会空转到轮数上限。
1045
+ # 比较步骤前后的未消费数即可区分。
952
1046
  before_n = len(_pending_messages(run_id))
953
- res = _run_step(run_id, "direct" if first else "chat", impl, prompt, step_wd,
954
- readonly=False, ev=ev, note=note,
955
- resume=sid or None, images=images)
1047
+ if bi is not None:
1048
+ res = _run_builtin_step(run_id, "direct" if first else "chat", bi, prompt,
1049
+ step_wd, ev=ev, note=note)
1050
+ else:
1051
+ res = _run_step(run_id, "direct" if first else "chat", impl, prompt, step_wd,
1052
+ readonly=False, ev=ev, note=note,
1053
+ resume=sid or None, images=images)
956
1054
  if not res["ok"]:
957
1055
  store.update_run(run_id, status="failed",
958
1056
  error="执行失败: %s" % res.get("error"), ended_at=_now())
959
1057
  return
960
1058
  turns += 1
961
1059
  last_text = (res.get("text") or "").strip()
962
- new_sid = _resume_sid(impl, res.get("sid"))
963
- if new_sid:
964
- sid = new_sid
1060
+ if impl is not None:
1061
+ new_sid = _resume_sid(impl, res.get("sid"))
1062
+ if new_sid:
1063
+ sid = new_sid
965
1064
  try:
966
- store.update_run(run_id, direct_session={"agent": impl["id"], "session": sid,
967
- "workdir": step_wd})
1065
+ store.update_run(run_id, direct_session={
1066
+ "agent": "builtin" if bi is not None else impl["id"],
1067
+ "session": sid, "workdir": step_wd})
968
1068
  except Exception:
969
1069
  pass
970
1070
  if turns >= DIRECT_MAX_TURNS:
@@ -974,9 +1074,12 @@ def _run_direct(run, task, agents, ev, stats, mode):
974
1074
  first = False
975
1075
 
976
1076
  verdict = {"type": task["type"], "engine": "direct", "pass": True, "mode": mode,
977
- "direct": True, "turns": turns, "impl": impl["id"], "route": route}
1077
+ "direct": True, "turns": turns,
1078
+ "impl": "builtin" if bi is not None else impl["id"], "route": route}
1079
+ impl_label = ("内置智能体(%s · %s)" % (bi["provider_name"], bi["model"])
1080
+ if bi is not None else impl.get("label"))
978
1081
  report = ["# 直连任务:%s" % task["title"], "",
979
- "- 执行者:%s(%d 轮对话)" % (impl.get("label"), turns), ""]
1082
+ "- 执行者:%s(%d 轮对话)" % (impl_label, turns), ""]
980
1083
  if last_text:
981
1084
  report += ["## 最近一轮输出", "", last_text[-5000:], ""]
982
1085
  store.write_report(run_id, "\n".join(report))
package/app/main.py CHANGED
@@ -1142,14 +1142,13 @@ class Handler(BaseHTTPRequestHandler):
1142
1142
  "id": m.get("id"),
1143
1143
  })
1144
1144
  for s in (run.get("steps") or []):
1145
- # 正文优先取步骤记录里的 output(runner 抽好的最终回答,干净);
1146
- # run 没有该字段时退回日志——日志是全量事件流(含【思考】【命令】),
1147
- # 塞进对话气泡就成了「看日志」,只作兜底。
1145
+ # 正文只认 output(runner 抽好的最终回答)→ summary。绝不回退读原始
1146
+ # 日志:日志是全量事件流(下发提示词回显 + CLI 报错),塞进气泡就成了
1147
+ # 「看日志」(2026-09-17 实测症状);运行中的步骤两者都还没有,留空给
1148
+ # 前端显示「正在执行」占位。
1148
1149
  body = s.get("output") or ""
1149
1150
  if not body:
1150
1151
  body = s.get("summary") or ""
1151
- if not body and s.get("log"):
1152
- body = store.read_step_log(run_id, s["log"], tail=8000, pretty=True)
1153
1152
  items.append({
1154
1153
  "kind": "agent",
1155
1154
  "at": s.get("ended_at") or s.get("started_at") or "",
@@ -1343,6 +1342,13 @@ def main():
1343
1342
 
1344
1343
  # 启动每步都落一行进度(flush 强制落屏):冷启动在慢盘/杀软扫描下可能几十秒,
1345
1344
  # 不打印会让用户以为卡死(真实案例:npm 装完首启只看到横幅像挂了)。
1345
+ # 看门狗:任何阶段卡超过 20 秒,自动把所有线程堆栈打到控制台(每 20s 重复)——
1346
+ # 用户截图即可精确定位卡点,不用猜。
1347
+ try:
1348
+ import faulthandler
1349
+ faulthandler.dump_traceback_later(20, repeat=True, file=sys.stderr)
1350
+ except Exception:
1351
+ pass
1346
1352
  _t0 = time.time()
1347
1353
 
1348
1354
  def _step(msg):
@@ -1431,6 +1437,11 @@ def main():
1431
1437
  print("[CodeBee] 本机 http://127.0.0.1:%d" % args.port, flush=True)
1432
1438
  print("[CodeBee] 数据目录 %s" % paths.DATA_DIR, flush=True)
1433
1439
  print("[CodeBee] ✔ 已就绪,浏览器即将自动打开;不要关闭本窗口。", flush=True)
1440
+ try:
1441
+ import faulthandler
1442
+ faulthandler.cancel_dump_traceback_later() # 启动完成,看门狗退役(否则运行期每 20s 误报堆栈)
1443
+ except Exception:
1444
+ pass
1434
1445
  if remote.PUBLIC_URL:
1435
1446
  print("[CodeBee] 公网 %s/?token=%s ← 任何网络可访问(反代回源已强制校验令牌)"
1436
1447
  % (remote.PUBLIC_URL, tok))
package/app/ui/app.js CHANGED
@@ -5029,11 +5029,13 @@ async function renderChat(run, active) {
5029
5029
  }
5030
5030
  // 协议尾行是给编排器判收的机器标记,不该出现在聊天正文里(真源在提示词约定)
5031
5031
  const body = String(it.text || "").replace(/\n*DIRECT_DONE:.*\s*$/s, "").trim();
5032
+ // 运行中的步骤还没有正文(output/summary 都空):给占位,不渲染空气泡
5033
+ const shown = body || (it.status === "running" ? t("正在执行…") : t("(本轮无文本输出)"));
5032
5034
  // 智能体回复:头像 + 整幅正文(不套气泡框),元信息小字落在正文下方
5033
5035
  return '<div class="chat-row">' +
5034
5036
  '<span class="chat-avatar" aria-hidden="true"><svg class="ico"><use href="#i-bee"></use></svg></span>' +
5035
5037
  '<div class="chat-bubble agent">' +
5036
- '<pre class="chat-body">' + esc(body) + "</pre>" +
5038
+ '<pre class="chat-body">' + esc(shown) + "</pre>" +
5037
5039
  '<div class="chat-meta">' + esc(it.who || "") + " " + esc(it.at || "") +
5038
5040
  (it.note ? " · " + esc(it.note) : "") +
5039
5041
  (it.status && it.status !== "done" ? " · " + esc(it.status) : "") + "</div></div></div>";
package/app/ui/i18n.js CHANGED
@@ -263,6 +263,8 @@
263
263
  "操作失败:": "Operation failed: ",
264
264
  "暂停": "Pause",
265
265
  "继续执行": "Resume",
266
+ "正在执行…": "Working…",
267
+ "(本轮无文本输出)": "(no text output this turn)",
266
268
  "也可直接粘贴截图": "or paste a screenshot directly",
267
269
  "添加附件": "Add attachment",
268
270
  "添加附件:截图/文本/代码/PDF 等,创建任务时落盘到工作目录 _attachments/": "Add attachments — screenshots/text/code/PDF; saved to the task workdir under _attachments/ on creation",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebee",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "CodeBee · 多智能体编排台 — 统一编排本机 AI 编码 CLI(Codex / Claude Code / QwenCode / OpenCode / Aider…):目标→拆解→路由→执行→验证→跨厂商评审→修复→报告。纯 Python 标准库实现,本地 Web UI。",
5
5
  "license": "MIT",
6
6
  "bin": {