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 CHANGED
@@ -6,6 +6,12 @@ README 元数据带回,供老版本在「发现新版本」时展示新版更
6
6
 
7
7
  ## 未发布
8
8
 
9
+ ## v0.1.41(2026-09-22)
10
+
11
+ ### 🛠 问题修复
12
+
13
+ - 断点续跑/重跑更省钱:同一任务再次生成大纲或执行计划时直接命中上次结果的精确缓存(1 小时内有效),不再重复花一次全量规划调用;大纲首试成功但格式不合规时的重试仍会重新生成(保「换一次样本」语义,不会拿同一份坏文本空转)。
14
+
9
15
  ## v0.1.40(2026-09-21)
10
16
 
11
17
  ### 🛠 问题修复
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
 
@@ -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
- res = modelhub.chat(prov["id"], model, prompt,
331
- max_tokens=16000, timeout=300, on_delta=cb,
332
- reasoning_effort=_reasoning_effort(task))
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebee",
3
- "version": "0.1.40",
3
+ "version": "0.1.41",
4
4
  "description": "CodeBee · 多智能体编排台 — 统一编排本机 AI 编码 CLI(Codex / Claude Code / QwenCode / OpenCode / Aider…):目标→拆解→路由→执行→验证→跨厂商评审→修复→报告。纯 Python 标准库实现,本地 Web UI。",
5
5
  "license": "MIT",
6
6
  "bin": {