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.
@@ -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)
@@ -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
- """直连引擎:目标+附件直接交给一个 CLI,跑完即止。
886
-
887
- 无规划/评审/验证/换将——快档位,质量交给执行 CLI 自身。对话式续轮:
888
- 运行中信箱来消息 drain 注入下一步(_run_step 既有机制);步骤结束后
889
- 信箱还有未消费消息就再续一轮(会话续接,同一 CLI 连续对话);信箱空了
890
- 收工为 done。运行结束后再来消息走 retry_task(消息自动继承到新 run)。
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
- 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:
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
- # 的下一轮而非首轮。继承上一轮的 CLI 会话 id,让它真的「接着上次聊」;
914
- # 同时把首步切成续轮档(DIRECT_FOLLOWUP),避免又走一遍开场的完整任务框架。
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
- prompt = (DIRECT_PROMPT
934
- .replace("__GOAL__", task["goal"])
935
- .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 "(无)"))
936
1027
  note = route.get("implementer", "")
937
1028
  images = _task_images(task, workdir)
938
1029
  else:
939
- prompt = DIRECT_FOLLOWUP_PROMPT.replace("__GOAL__", task["goal"])
940
- if not sid and last_text:
941
- # 无会话续接能力的 CLI(如 dsh 一次性任务):把上一轮输出尾部带进上下文
942
- 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:]
943
1039
  note = "对话续轮"
944
1040
  images = None
945
1041
  # 续轮判据:只有「本步执行期间新到」的消息才再开一轮。
946
- # 不能只看「信箱非空」——真实步骤的 drain 在 _run_step 内部发生,起跑前
947
- # 就积压的消息会被本步吃掉(peek 归零);而 mock/不走 drain 的路径消息
948
- # 永远不消费,只看非空会空转到轮数上限。比较步骤前后的未消费数即可区分。
1042
+ # 不能只看「信箱非空」——真实步骤的 drain 在 _run_step/_run_builtin_step
1043
+ # 内部发生,起跑前就积压的消息会被本步吃掉(peek 归零);而 mock/不走
1044
+ # drain 的路径消息永远不消费,只看非空会空转到轮数上限。
1045
+ # 比较步骤前后的未消费数即可区分。
949
1046
  before_n = len(_pending_messages(run_id))
950
- res = _run_step(run_id, "direct" if first else "chat", impl, prompt, step_wd,
951
- readonly=False, ev=ev, note=note,
952
- 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)
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
- new_sid = _resume_sid(impl, res.get("sid"))
960
- if new_sid:
961
- 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
962
1064
  try:
963
- store.update_run(run_id, direct_session={"agent": impl["id"], "session": sid,
964
- "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})
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, "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"))
975
1081
  report = ["# 直连任务:%s" % task["title"], "",
976
- "- 执行者:%s(%d 轮对话)" % (impl.get("label"), turns), ""]
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