codebee 0.1.40 → 0.1.41
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/CHANGELOG.md +6 -0
- package/README.md +3 -0
- package/app/core/planner.py +98 -90
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -23,6 +23,9 @@ Kimi Code、MiMo Code、Grok Build、Pi、DeepSeek Harness……),提供
|
|
|
23
23
|
<!-- relnotes:start -->
|
|
24
24
|
### 🛠 问题修复 · Fixes
|
|
25
25
|
|
|
26
|
+
- 断点续跑/重跑更省钱:同任务再次生成大纲或计划直接命中精确缓存(1 小时内),不重复花全量规划调用;格式不合规的重试仍重新生成。
|
|
27
|
+
- Resuming or re-running a task now hits an exact cache for outline/plan regeneration (within 1 hour), skipping a full planning call; format-failure retries still regenerate.
|
|
28
|
+
|
|
26
29
|
- 修复长任务上下文撑爆后重试仍失败:压缩摘要此前只写日志、没真正替换发往模型的上下文;现在重试用「压缩摘要+最近消息」重建请求并放弃膨胀的旧会话,超长任务能真正续跑。
|
|
27
30
|
- Fixed long-task overflow retries failing again: compaction summaries used to go only to logs. Retries now rebuild the request from the summary plus recent messages and drop the bloated old session.
|
|
28
31
|
|
package/app/core/planner.py
CHANGED
|
@@ -114,13 +114,13 @@ def _log_usage(source, role, task, res, agent=None, tool="", model="", provider=
|
|
|
114
114
|
except Exception:
|
|
115
115
|
pass
|
|
116
116
|
|
|
117
|
-
CODE_PLAN_PROMPT = """你是技术负责人。请把下面的开发目标拆解为 __N__ 个以内、按顺序执行的子任务,
|
|
118
|
-
并判定任务难度和后续质量步骤。只输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
|
|
119
|
-
{"difficulty": "easy 或 hard", "workflow": {"review_required": true, "max_repair_rounds": 0, "allow_switch": false}, "subtasks": [{"title": "简短标题", "detail": "具体要做什么,给执行工程师的直接指令", "files": ["涉及的文件路径"]}]}
|
|
120
|
-
难度判定:常规增删改查/小函数/格式调整 = easy;跨模块改动/架构调整/复杂算法/安全相关 = hard。
|
|
121
|
-
质量步骤判定:涉及安全、数据迁移、并发、权限、公共 API 或跨模块改动时必须评审;
|
|
122
|
-
有明确自动验收的低风险小改可不做模型评审。max_repair_rounds 取 0-2,只有复杂任务才建议换将。
|
|
123
|
-
子任务粒度要可独立验证;最后一个子任务必须包含整体联调/收尾。
|
|
117
|
+
CODE_PLAN_PROMPT = """你是技术负责人。请把下面的开发目标拆解为 __N__ 个以内、按顺序执行的子任务,
|
|
118
|
+
并判定任务难度和后续质量步骤。只输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
|
|
119
|
+
{"difficulty": "easy 或 hard", "workflow": {"review_required": true, "max_repair_rounds": 0, "allow_switch": false}, "subtasks": [{"title": "简短标题", "detail": "具体要做什么,给执行工程师的直接指令", "files": ["涉及的文件路径"]}]}
|
|
120
|
+
难度判定:常规增删改查/小函数/格式调整 = easy;跨模块改动/架构调整/复杂算法/安全相关 = hard。
|
|
121
|
+
质量步骤判定:涉及安全、数据迁移、并发、权限、公共 API 或跨模块改动时必须评审;
|
|
122
|
+
有明确自动验收的低风险小改可不做模型评审。max_repair_rounds 取 0-2,只有复杂任务才建议换将。
|
|
123
|
+
子任务粒度要可独立验证;最后一个子任务必须包含整体联调/收尾。
|
|
124
124
|
|
|
125
125
|
## 先探索再计划(重要,借鉴 OpenSpec explore)
|
|
126
126
|
拆解之前先用你的读文件/搜索工具**实际查看工作目录**,找到目标相关的真实文件与函数,再据此拆解。
|
|
@@ -137,12 +137,12 @@ __CONTEXT__
|
|
|
137
137
|
## 验收命令(最终必须通过)
|
|
138
138
|
__VERIFY__"""
|
|
139
139
|
|
|
140
|
-
REVIEW_OUTLINE_PROMPT = """你是内容主编。请为下面的创作任务拟一份写作大纲(要点列表,3-8 条),
|
|
141
|
-
并按内容风险建议后续质量步骤。
|
|
142
|
-
只输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
|
|
143
|
-
{"outline": ["要点1", "要点2", ...], "workflow": {"reviewers": 1, "review_rounds": 1}}
|
|
144
|
-
reviewers 取 1-2,review_rounds 取 1-3;研究报告、技术方案、长文和高发布门槛应增加,
|
|
145
|
-
普通邮件、短翻译、工作汇报等低风险短内容保持 1。
|
|
140
|
+
REVIEW_OUTLINE_PROMPT = """你是内容主编。请为下面的创作任务拟一份写作大纲(要点列表,3-8 条),
|
|
141
|
+
并按内容风险建议后续质量步骤。
|
|
142
|
+
只输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
|
|
143
|
+
{"outline": ["要点1", "要点2", ...], "workflow": {"reviewers": 1, "review_rounds": 1}}
|
|
144
|
+
reviewers 取 1-2,review_rounds 取 1-3;研究报告、技术方案、长文和高发布门槛应增加,
|
|
145
|
+
普通邮件、短翻译、工作汇报等低风险短内容保持 1。
|
|
146
146
|
|
|
147
147
|
## 创作任务
|
|
148
148
|
__GOAL__
|
|
@@ -327,9 +327,14 @@ def make_serial_outline(task, author_agent=None, workdir=None, ev=None, log_path
|
|
|
327
327
|
# 否则 stop_reason=max_tokens、正文为空(实测 2048 全被思考吞掉)
|
|
328
328
|
# 流式回调把增量实时写进步骤日志,直连生成不再是一行标题的黑箱
|
|
329
329
|
cb = _log_streamer(log_path)
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
330
|
+
# §07 T2.2 幂等精确缓存(TTL 1h):断点续跑/重跑同任务时大纲
|
|
331
|
+
# prompt 逐字节相同,命中省一次全量规划调用。第 2 次尝试故意
|
|
332
|
+
# 旁路缓存——首试 ok 但 JSON 不合法时,重试要的是「再抽一次样
|
|
333
|
+
# 本」而不是同一份坏文本。
|
|
334
|
+
res = modelhub.chat(prov["id"], model, prompt,
|
|
335
|
+
max_tokens=16000, timeout=300, on_delta=cb,
|
|
336
|
+
reasoning_effort=_reasoning_effort(task),
|
|
337
|
+
cache_ttl=3600 if _attempt == 1 else 0)
|
|
333
338
|
cb.flush()
|
|
334
339
|
_log_usage("outline", "outline", task, res, model=model,
|
|
335
340
|
provider=prov.get("name", prov.get("id", "")),
|
|
@@ -409,56 +414,56 @@ def _norm_subtasks(data):
|
|
|
409
414
|
return steps or None
|
|
410
415
|
|
|
411
416
|
|
|
412
|
-
def _plan_difficulty(data):
|
|
417
|
+
def _plan_difficulty(data):
|
|
413
418
|
d = str((data or {}).get("difficulty") or "").lower()
|
|
414
|
-
return d if d in ("easy", "hard") else None
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
def _plan_workflow(data):
|
|
418
|
-
"""规范化规划器给出的后续步骤建议;最终安全下限由任务编译器裁决。"""
|
|
419
|
-
raw = (data or {}).get("workflow")
|
|
420
|
-
if not isinstance(raw, dict):
|
|
421
|
-
return None
|
|
422
|
-
out = {}
|
|
423
|
-
if isinstance(raw.get("review_required"), bool):
|
|
424
|
-
out["review_required"] = raw["review_required"]
|
|
425
|
-
try:
|
|
426
|
-
if "max_repair_rounds" in raw:
|
|
427
|
-
out["max_repair_rounds"] = max(0, min(2, int(raw["max_repair_rounds"])))
|
|
428
|
-
except (TypeError, ValueError):
|
|
429
|
-
pass
|
|
430
|
-
if isinstance(raw.get("allow_switch"), bool):
|
|
431
|
-
out["allow_switch"] = raw["allow_switch"]
|
|
432
|
-
return out or None
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
def _orchestrator():
|
|
436
|
-
"""编排者可用时返回 (provider, model),否则 None。"""
|
|
437
|
-
try:
|
|
438
|
-
return modelhub.resolve_orchestrator()
|
|
439
|
-
except Exception:
|
|
440
|
-
return None
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
def _reasoning_effort(task):
|
|
444
|
-
"""把任务级思考程度映射为直连编排者支持的 reasoning 参数。"""
|
|
445
|
-
task = task if isinstance(task, dict) else {}
|
|
446
|
-
selected = str(task.get("thinking") or "standard").strip().lower()
|
|
447
|
-
if selected in ("low", "standard", "high"):
|
|
448
|
-
return {"low": "low", "standard": "medium", "high": "high"}[selected]
|
|
449
|
-
mode = str(task.get("mode") or "auto").lower()
|
|
450
|
-
difficulty = str(task.get("difficulty") or "auto").lower()
|
|
451
|
-
ttype = str(task.get("type") or "").lower()
|
|
452
|
-
try:
|
|
453
|
-
threshold = float(task.get("threshold") or 7.0)
|
|
454
|
-
except (TypeError, ValueError):
|
|
455
|
-
threshold = 7.0
|
|
456
|
-
if mode == "fast" or difficulty == "easy" or threshold <= 6.0:
|
|
457
|
-
return "low"
|
|
458
|
-
if (mode == "expert" or difficulty == "hard" or threshold >= 8.5
|
|
459
|
-
or ttype in ("research", "tech_proposal", "serial_novel")):
|
|
460
|
-
return "high"
|
|
461
|
-
return "medium"
|
|
419
|
+
return d if d in ("easy", "hard") else None
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _plan_workflow(data):
|
|
423
|
+
"""规范化规划器给出的后续步骤建议;最终安全下限由任务编译器裁决。"""
|
|
424
|
+
raw = (data or {}).get("workflow")
|
|
425
|
+
if not isinstance(raw, dict):
|
|
426
|
+
return None
|
|
427
|
+
out = {}
|
|
428
|
+
if isinstance(raw.get("review_required"), bool):
|
|
429
|
+
out["review_required"] = raw["review_required"]
|
|
430
|
+
try:
|
|
431
|
+
if "max_repair_rounds" in raw:
|
|
432
|
+
out["max_repair_rounds"] = max(0, min(2, int(raw["max_repair_rounds"])))
|
|
433
|
+
except (TypeError, ValueError):
|
|
434
|
+
pass
|
|
435
|
+
if isinstance(raw.get("allow_switch"), bool):
|
|
436
|
+
out["allow_switch"] = raw["allow_switch"]
|
|
437
|
+
return out or None
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _orchestrator():
|
|
441
|
+
"""编排者可用时返回 (provider, model),否则 None。"""
|
|
442
|
+
try:
|
|
443
|
+
return modelhub.resolve_orchestrator()
|
|
444
|
+
except Exception:
|
|
445
|
+
return None
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _reasoning_effort(task):
|
|
449
|
+
"""把任务级思考程度映射为直连编排者支持的 reasoning 参数。"""
|
|
450
|
+
task = task if isinstance(task, dict) else {}
|
|
451
|
+
selected = str(task.get("thinking") or "standard").strip().lower()
|
|
452
|
+
if selected in ("low", "standard", "high"):
|
|
453
|
+
return {"low": "low", "standard": "medium", "high": "high"}[selected]
|
|
454
|
+
mode = str(task.get("mode") or "auto").lower()
|
|
455
|
+
difficulty = str(task.get("difficulty") or "auto").lower()
|
|
456
|
+
ttype = str(task.get("type") or "").lower()
|
|
457
|
+
try:
|
|
458
|
+
threshold = float(task.get("threshold") or 7.0)
|
|
459
|
+
except (TypeError, ValueError):
|
|
460
|
+
threshold = 7.0
|
|
461
|
+
if mode == "fast" or difficulty == "easy" or threshold <= 6.0:
|
|
462
|
+
return "low"
|
|
463
|
+
if (mode == "expert" or difficulty == "hard" or threshold >= 8.5
|
|
464
|
+
or ttype in ("research", "tech_proposal", "serial_novel")):
|
|
465
|
+
return "high"
|
|
466
|
+
return "medium"
|
|
462
467
|
|
|
463
468
|
|
|
464
469
|
def make_code_plan(task, planner_agent, workdir, ev=None, resume=None, log_path=None):
|
|
@@ -482,10 +487,10 @@ def make_code_plan(task, planner_agent, workdir, ev=None, resume=None, log_path=
|
|
|
482
487
|
_log_usage("plan", "plan", task, res, agent=planner_agent)
|
|
483
488
|
data = runner.extract_json(res.get("text") or "")
|
|
484
489
|
steps = _norm_subtasks(data)
|
|
485
|
-
if steps:
|
|
486
|
-
return {"source": "llm(%s)" % planner_agent["id"], "steps": steps,
|
|
487
|
-
"difficulty": _plan_difficulty(data),
|
|
488
|
-
"workflow": _plan_workflow(data)}
|
|
490
|
+
if steps:
|
|
491
|
+
return {"source": "llm(%s)" % planner_agent["id"], "steps": steps,
|
|
492
|
+
"difficulty": _plan_difficulty(data),
|
|
493
|
+
"workflow": _plan_workflow(data)}
|
|
489
494
|
return _fallback_code_plan(task, "LLM 计划解析失败,退化为单步模板")
|
|
490
495
|
|
|
491
496
|
|
|
@@ -496,13 +501,15 @@ def _fallback_code_plan(task, note):
|
|
|
496
501
|
|
|
497
502
|
def _orch_code_plan(task, prov, model, log_path=None):
|
|
498
503
|
cb = _log_streamer(log_path)
|
|
504
|
+
# §07 T2.2:代码计划同任务同 prompt(断点续跑/重试),TTL 1h 精确缓存
|
|
499
505
|
res = modelhub.chat(prov["id"], model,
|
|
500
506
|
(CODE_PLAN_PROMPT.replace("__N__", str(MAX_SUBTASKS))
|
|
501
507
|
.replace("__GOAL__", task["goal"])
|
|
502
508
|
.replace("__CONTEXT__", task.get("context") or "(无)")
|
|
503
509
|
.replace("__VERIFY__", task.get("verify_command") or "(未配置)")),
|
|
504
|
-
max_tokens=8000, timeout=300, on_delta=cb,
|
|
505
|
-
reasoning_effort=_reasoning_effort(task)
|
|
510
|
+
max_tokens=8000, timeout=300, on_delta=cb,
|
|
511
|
+
reasoning_effort=_reasoning_effort(task),
|
|
512
|
+
cache_ttl=3600)
|
|
506
513
|
cb.flush()
|
|
507
514
|
_log_usage("plan", "plan", task, res, model=model,
|
|
508
515
|
provider=prov.get("name", prov.get("id", "")))
|
|
@@ -515,9 +522,9 @@ def _orch_code_plan(task, prov, model, log_path=None):
|
|
|
515
522
|
if not steps:
|
|
516
523
|
_append_log(log_path, "编排者计划返回内容无法解析为子任务,回退 CLI")
|
|
517
524
|
return None
|
|
518
|
-
return {"source": "编排者(%s · %s)" % (prov.get("name", prov["id"]), model),
|
|
519
|
-
"steps": steps, "difficulty": _plan_difficulty(data),
|
|
520
|
-
"workflow": _plan_workflow(data)}
|
|
525
|
+
return {"source": "编排者(%s · %s)" % (prov.get("name", prov["id"]), model),
|
|
526
|
+
"steps": steps, "difficulty": _plan_difficulty(data),
|
|
527
|
+
"workflow": _plan_workflow(data)}
|
|
521
528
|
|
|
522
529
|
|
|
523
530
|
def make_review_outline(task):
|
|
@@ -530,8 +537,9 @@ def make_review_outline(task):
|
|
|
530
537
|
(REVIEW_OUTLINE_PROMPT
|
|
531
538
|
.replace("__GOAL__", task["goal"])
|
|
532
539
|
.replace("__CONTEXT__", task.get("context") or "(无)")),
|
|
533
|
-
max_tokens=8000, timeout=300,
|
|
534
|
-
reasoning_effort=_reasoning_effort(task)
|
|
540
|
+
max_tokens=8000, timeout=300,
|
|
541
|
+
reasoning_effort=_reasoning_effort(task),
|
|
542
|
+
cache_ttl=3600) # §07 T2.2:同任务重试幂等,TTL 1h
|
|
535
543
|
_log_usage("outline", "outline", task, res, model=model,
|
|
536
544
|
provider=prov.get("name", prov.get("id", "")))
|
|
537
545
|
if not res["ok"]:
|
|
@@ -541,20 +549,20 @@ def make_review_outline(task):
|
|
|
541
549
|
if not isinstance(outline, list):
|
|
542
550
|
return None
|
|
543
551
|
items = [str(x).strip()[:120] for x in outline if str(x).strip()][:8]
|
|
544
|
-
if not items:
|
|
545
|
-
return None
|
|
546
|
-
workflow = data.get("workflow") if isinstance(data.get("workflow"), dict) else {}
|
|
547
|
-
try:
|
|
548
|
-
reviewers = max(1, min(2, int(workflow.get("reviewers") or 1)))
|
|
549
|
-
except (TypeError, ValueError):
|
|
550
|
-
reviewers = 1
|
|
551
|
-
try:
|
|
552
|
-
review_rounds = max(1, min(3, int(workflow.get("review_rounds") or 1)))
|
|
553
|
-
except (TypeError, ValueError):
|
|
554
|
-
review_rounds = 1
|
|
555
|
-
return {"source": "编排者(%s · %s)" % (prov.get("name", prov["id"]), model),
|
|
556
|
-
"items": items,
|
|
557
|
-
"workflow": {"reviewers": reviewers, "review_rounds": review_rounds}}
|
|
552
|
+
if not items:
|
|
553
|
+
return None
|
|
554
|
+
workflow = data.get("workflow") if isinstance(data.get("workflow"), dict) else {}
|
|
555
|
+
try:
|
|
556
|
+
reviewers = max(1, min(2, int(workflow.get("reviewers") or 1)))
|
|
557
|
+
except (TypeError, ValueError):
|
|
558
|
+
reviewers = 1
|
|
559
|
+
try:
|
|
560
|
+
review_rounds = max(1, min(3, int(workflow.get("review_rounds") or 1)))
|
|
561
|
+
except (TypeError, ValueError):
|
|
562
|
+
review_rounds = 1
|
|
563
|
+
return {"source": "编排者(%s · %s)" % (prov.get("name", prov["id"]), model),
|
|
564
|
+
"items": items,
|
|
565
|
+
"workflow": {"reviewers": reviewers, "review_rounds": review_rounds}}
|
|
558
566
|
|
|
559
567
|
|
|
560
568
|
def make_novel_plan(task, author, critics):
|
package/package.json
CHANGED