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.
- package/app/core/builtin_agent.py +382 -0
- package/app/core/flows.py +1 -1
- package/app/core/jobs.py +441 -424
- package/app/core/pipeline.py +134 -31
- package/app/main.py +16 -5
- package/app/ui/app.js +3 -1
- package/app/ui/i18n.js +2 -0
- package/package.json +1 -1
package/app/core/pipeline.py
CHANGED
|
@@ -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
|
-
"""
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
917
|
-
#
|
|
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
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
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
|
-
#
|
|
951
|
-
#
|
|
1042
|
+
# 不能只看「信箱非空」——真实步骤的 drain 在 _run_step/_run_builtin_step
|
|
1043
|
+
# 内部发生,起跑前就积压的消息会被本步吃掉(peek 归零);而 mock/不走
|
|
1044
|
+
# drain 的路径消息永远不消费,只看非空会空转到轮数上限。
|
|
1045
|
+
# 比较步骤前后的未消费数即可区分。
|
|
952
1046
|
before_n = len(_pending_messages(run_id))
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
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
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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={
|
|
967
|
-
|
|
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,
|
|
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 轮对话)" % (
|
|
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
|
-
#
|
|
1146
|
-
#
|
|
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(
|
|
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