codebee 0.1.2 → 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/bookmeta.py +161 -27
- package/app/core/bookmeta_catalog.py +187 -0
- 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/manager.py +1523 -1511
- package/app/core/market_remote.py +959 -896
- package/app/core/modelhub.py +7 -0
- package/app/core/pipeline.py +138 -32
- package/app/core/store.py +3 -1
- package/app/main.py +1478 -1448
- package/app/ui/app.js +212 -20
- package/app/ui/i18n.js +1732 -1712
- package/app/ui/index.html +6 -2
- package/app/ui/style.css +87 -59
- package/bin/tutti.js +147 -147
- package/package.json +39 -39
package/app/core/modelhub.py
CHANGED
|
@@ -949,6 +949,13 @@ def set_binding(agent_id, provider_id=None, model=None, models=None,
|
|
|
949
949
|
_sync_chain_refs(b)
|
|
950
950
|
if provider_id is not None:
|
|
951
951
|
b["provider_id"] = eff_pid # 显式指定主供应商时覆盖链首推导
|
|
952
|
+
# 链首非空时主供应商跟链首走:旧读方/下拉残留的 provider_id
|
|
953
|
+
# 若与链首不一致,会复活成「下拉=停用的旧供应商」(2026-09-16
|
|
954
|
+
# 一键推荐实测)。链首为空(纯 CLI 默认凭据)才保留显式指定。
|
|
955
|
+
head_pid = next((c.get("provider_id") for c in b["chain"]
|
|
956
|
+
if c.get("provider_id")), "")
|
|
957
|
+
if head_pid:
|
|
958
|
+
b["provider_id"] = head_pid
|
|
952
959
|
elif models is not None:
|
|
953
960
|
# 有序模型链:第 1 个主模型,其余按序降级
|
|
954
961
|
clean = _clean_models(models)
|
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
|
|
|
@@ -417,7 +467,10 @@ def _finish_step_result(run_id, step, res, role, agent, start):
|
|
|
417
467
|
cost_usd=res.get("cost_usd", 0.0),
|
|
418
468
|
tokens=res.get("tokens", 0),
|
|
419
469
|
duration_s=time.time() - start,
|
|
420
|
-
model=res.get("model")
|
|
470
|
+
model=res.get("model"),
|
|
471
|
+
# 智能体的最终回答(runner 已从 JSONL 事件流里抽出 agent_message)。
|
|
472
|
+
# 对话视图直读这个;日志文件是全量事件流,塞进气泡就成了「看日志」。
|
|
473
|
+
output=(res.get("text") or ""))
|
|
421
474
|
if agent.get("mode") != "mock":
|
|
422
475
|
_record_usage(run_id, role, agent, res, source="pipeline", step=step["n"])
|
|
423
476
|
# 5F:step 级运行时断言(只告警不阻断)
|
|
@@ -848,6 +901,28 @@ DIRECT_DONE: <一句话说明本轮做了什么>
|
|
|
848
901
|
|
|
849
902
|
DIRECT_MAX_TURNS = 200 # 对话续轮上限(每轮都要用户主动发消息才触发,防意外打满)
|
|
850
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
|
+
|
|
851
926
|
|
|
852
927
|
def _pending_messages(run_id):
|
|
853
928
|
"""该 run 信箱里未消费消息列表(读不到时当空,绝不因信箱异常打断执行)。"""
|
|
@@ -882,25 +957,36 @@ def _direct_last_text(run):
|
|
|
882
957
|
|
|
883
958
|
|
|
884
959
|
def _run_direct(run, task, agents, ev, stats, mode):
|
|
885
|
-
"""
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
960
|
+
"""直连引擎:目标+附件直接交给一个执行者,跑完即止。
|
|
961
|
+
|
|
962
|
+
执行者优先级:内置智能体(直连模型 API + 工具循环,无 CLI 进程)→ CLI 智能体。
|
|
963
|
+
任务显式声明 CLI 会话续接(resume)或手动指定了执行者时尊重选择走 CLI;
|
|
964
|
+
无可用供应商时回退 CLI。无规划/评审/验证/换将——快档位。对话式续轮:
|
|
965
|
+
运行中信箱来消息 → drain 注入下一步;步骤结束后信箱还有未消费消息就
|
|
966
|
+
再续一轮;信箱空了收工为 done。运行结束后再来消息走 retry_task
|
|
967
|
+
(消息自动继承到新 run)。
|
|
891
968
|
"""
|
|
892
969
|
run_id = run["id"]
|
|
893
970
|
workdir = task["workdir"]
|
|
894
971
|
route = {}
|
|
895
972
|
resume_ctx = _valid_resume(task, agents)
|
|
896
|
-
|
|
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:
|
|
897
983
|
impl = resume_ctx["agent"]
|
|
898
984
|
route["implementer"] = resume_ctx["note"]
|
|
899
985
|
elif mode == "manual":
|
|
900
986
|
impl, _ = _pick_implementer(agents, task.get("implementer"))
|
|
901
987
|
else:
|
|
902
988
|
impl, route["implementer"] = router.pick(agents, "implement", task["type"], stats)
|
|
903
|
-
if impl is None:
|
|
989
|
+
if impl is None and bi is None:
|
|
904
990
|
store.update_run(run_id, status="failed", error="没有可用智能体", ended_at=_now())
|
|
905
991
|
return
|
|
906
992
|
difficulty = task.get("difficulty") or "default"
|
|
@@ -910,8 +996,8 @@ def _run_direct(run, task, agents, ev, stats, mode):
|
|
|
910
996
|
sid = (resume_ctx["session"] if resume_ctx else "") or ""
|
|
911
997
|
last_text = ""
|
|
912
998
|
# 追话起跑(/api/runs/<id>/chat → retry_task):信箱已有未消费消息 = 这是对话
|
|
913
|
-
#
|
|
914
|
-
#
|
|
999
|
+
# 的下一轮而非首轮。CLI 继承上一轮的会话 id(真的「接着上次聊」),内置智能体
|
|
1000
|
+
# 靠「上一轮输出(结尾)」块带上下文;同时把首步切成续轮档。
|
|
915
1001
|
try:
|
|
916
1002
|
pending0 = store.peek_messages(run_id)
|
|
917
1003
|
except Exception:
|
|
@@ -920,7 +1006,7 @@ def _run_direct(run, task, agents, ev, stats, mode):
|
|
|
920
1006
|
prev = _direct_prev_run(task["id"], run_id)
|
|
921
1007
|
if prev:
|
|
922
1008
|
ps = (prev.get("direct_session") or {})
|
|
923
|
-
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"):
|
|
924
1010
|
sid = sid or ps["session"]
|
|
925
1011
|
if ps.get("workdir"):
|
|
926
1012
|
step_wd = ps["workdir"]
|
|
@@ -930,38 +1016,55 @@ def _run_direct(run, task, agents, ev, stats, mode):
|
|
|
930
1016
|
while True:
|
|
931
1017
|
_wait_gate(run_id, ev)
|
|
932
1018
|
if first:
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
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 "(无)"))
|
|
936
1027
|
note = route.get("implementer", "")
|
|
937
1028
|
images = _task_images(task, workdir)
|
|
938
1029
|
else:
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
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:]
|
|
943
1039
|
note = "对话续轮"
|
|
944
1040
|
images = None
|
|
945
1041
|
# 续轮判据:只有「本步执行期间新到」的消息才再开一轮。
|
|
946
|
-
# 不能只看「信箱非空」——真实步骤的 drain 在 _run_step
|
|
947
|
-
#
|
|
948
|
-
#
|
|
1042
|
+
# 不能只看「信箱非空」——真实步骤的 drain 在 _run_step/_run_builtin_step
|
|
1043
|
+
# 内部发生,起跑前就积压的消息会被本步吃掉(peek 归零);而 mock/不走
|
|
1044
|
+
# drain 的路径消息永远不消费,只看非空会空转到轮数上限。
|
|
1045
|
+
# 比较步骤前后的未消费数即可区分。
|
|
949
1046
|
before_n = len(_pending_messages(run_id))
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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)
|
|
953
1054
|
if not res["ok"]:
|
|
954
1055
|
store.update_run(run_id, status="failed",
|
|
955
1056
|
error="执行失败: %s" % res.get("error"), ended_at=_now())
|
|
956
1057
|
return
|
|
957
1058
|
turns += 1
|
|
958
1059
|
last_text = (res.get("text") or "").strip()
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1060
|
+
if impl is not None:
|
|
1061
|
+
new_sid = _resume_sid(impl, res.get("sid"))
|
|
1062
|
+
if new_sid:
|
|
1063
|
+
sid = new_sid
|
|
962
1064
|
try:
|
|
963
|
-
store.update_run(run_id, direct_session={
|
|
964
|
-
|
|
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})
|
|
965
1068
|
except Exception:
|
|
966
1069
|
pass
|
|
967
1070
|
if turns >= DIRECT_MAX_TURNS:
|
|
@@ -971,9 +1074,12 @@ def _run_direct(run, task, agents, ev, stats, mode):
|
|
|
971
1074
|
first = False
|
|
972
1075
|
|
|
973
1076
|
verdict = {"type": task["type"], "engine": "direct", "pass": True, "mode": mode,
|
|
974
|
-
"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"))
|
|
975
1081
|
report = ["# 直连任务:%s" % task["title"], "",
|
|
976
|
-
"- 执行者:%s(%d 轮对话)" % (
|
|
1082
|
+
"- 执行者:%s(%d 轮对话)" % (impl_label, turns), ""]
|
|
977
1083
|
if last_text:
|
|
978
1084
|
report += ["## 最近一轮输出", "", last_text[-5000:], ""]
|
|
979
1085
|
store.write_report(run_id, "\n".join(report))
|
package/app/core/store.py
CHANGED
|
@@ -1142,7 +1142,7 @@ def add_step(run_id, role, agent_id, agent_label, note=""):
|
|
|
1142
1142
|
|
|
1143
1143
|
|
|
1144
1144
|
def finish_step(run_id, n, status, summary="", exit_code=None,
|
|
1145
|
-
cost_usd=0.0, tokens=0.0, duration_s=None, model=None):
|
|
1145
|
+
cost_usd=0.0, tokens=0.0, duration_s=None, model=None, output=None):
|
|
1146
1146
|
with LOCK:
|
|
1147
1147
|
run = _RUNS.get(run_id)
|
|
1148
1148
|
if not run:
|
|
@@ -1159,6 +1159,8 @@ def finish_step(run_id, n, status, summary="", exit_code=None,
|
|
|
1159
1159
|
s["model"] = str(model)[:80]
|
|
1160
1160
|
if duration_s is not None:
|
|
1161
1161
|
s["duration_s"] = round(duration_s, 1)
|
|
1162
|
+
if output is not None:
|
|
1163
|
+
s["output"] = str(output)[:6000]
|
|
1162
1164
|
break
|
|
1163
1165
|
run["cost_usd"] = round(run.get("cost_usd", 0.0) + cost_usd, 4)
|
|
1164
1166
|
run["tokens"] = run.get("tokens", 0) + tokens
|