codebee 0.1.54 → 0.1.55

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.
@@ -30,17 +30,60 @@ from .repeat_guard import guard as repeat_guard
30
30
  DEFAULT_RUBRIC = ["情节", "人物", "文笔", "节奏", "吸引力"]
31
31
 
32
32
 
33
- class Cancelled(Exception):
34
- pass
33
+ class Cancelled(Exception):
34
+ pass
35
+
36
+
37
+ class TaskTimeout(Exception):
38
+ pass
35
39
 
36
40
 
37
41
  def _now():
38
42
  return time.strftime("%Y-%m-%d %H:%M:%S")
39
43
 
40
44
 
41
- def _check_cancel(ev):
42
- if ev is not None and ev.is_set():
43
- raise Cancelled()
45
+ def _check_cancel(ev):
46
+ if ev is not None and ev.is_set():
47
+ raise Cancelled()
48
+
49
+
50
+ def _run_deadline(run_id):
51
+ """Convert the persisted wall-clock cutoff into a monotonic deadline."""
52
+ run = store.get_run(run_id) or {}
53
+ try:
54
+ deadline_at = float(run.get("deadline_at"))
55
+ except (TypeError, ValueError):
56
+ return None
57
+ return time.monotonic() + (deadline_at - time.time())
58
+
59
+
60
+ def _ensure_budget(run_id):
61
+ deadline = _run_deadline(run_id)
62
+ if deadline is not None and time.monotonic() >= deadline:
63
+ store.update_run(run_id, expected_status="running", status="timeout",
64
+ ended_at=_now(), error="任务总时限已到")
65
+ raise TaskTimeout()
66
+ return deadline
67
+
68
+
69
+ def _step_timeout(run_id, requested, deadline=None):
70
+ value = max(0.0, float(requested or runner.DEFAULT_TIMEOUT))
71
+ deadline = deadline if deadline is not None else _run_deadline(run_id)
72
+ if deadline is not None:
73
+ value = min(value, max(0.0, deadline - time.monotonic()))
74
+ return value, deadline
75
+
76
+
77
+ def _planner_call(fn, *args, deadline=None, **kwargs):
78
+ """Allow older injected planner doubles to omit the new deadline kwarg."""
79
+ if deadline is None:
80
+ return fn(*args, **kwargs)
81
+ try:
82
+ return fn(*args, deadline=deadline, **kwargs)
83
+ except TypeError as exc:
84
+ if "unexpected keyword argument 'deadline'" not in str(exc):
85
+ raise
86
+ return fn(*args, **kwargs)
44
87
 
45
88
 
46
89
  def _inside(dirpath, target):
@@ -195,12 +238,12 @@ def _get_session(run_id):
195
238
  return s
196
239
 
197
240
 
198
- def _make_llm_caller(agent, workdir):
241
+ def _make_llm_caller(agent, workdir, deadline=None):
199
242
  """压缩摘要用 LLM:直接复用当前 step 的 agent(同 CLI 同模型)。"""
200
243
  def caller(messages):
201
244
  prompt = "\n\n".join(m.get("content", "") for m in messages)
202
- res = runner.run_agent(agent, prompt, workdir=workdir, readonly=True,
203
- timeout=300)
245
+ res = runner.run_agent(agent, prompt, workdir=workdir, readonly=True,
246
+ timeout=300, deadline=deadline)
204
247
  return res.get("text") or ""
205
248
  return caller
206
249
 
@@ -303,9 +346,11 @@ def _binding_dead_msg(agent):
303
346
  "请在「模型调度(可选)」页为该 CLI 指定已启用的供应商")
304
347
 
305
348
 
306
- def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner.DEFAULT_TIMEOUT, note="", resume=None, images=None, require_tools=False):
307
- """执行一个智能体步骤并记录。返回 runner 统一结果。"""
308
- _wait_gate(run_id, ev)
349
+ def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner.DEFAULT_TIMEOUT, note="", resume=None, images=None, require_tools=False):
350
+ """执行一个智能体步骤并记录。返回 runner 统一结果。"""
351
+ _wait_gate(run_id, ev)
352
+ deadline = _ensure_budget(run_id)
353
+ timeout, deadline = _step_timeout(run_id, timeout, deadline)
309
354
  # 绑定解析为空分两种(2026-09-18 起 区分对待):
310
355
  # · 从没配过链(binding_configured=False):回落 CLI 本机默认照跑——
311
356
  # 用户根本没在 CodeBee 里配供应商,谈不到「烧自己配的配额」;判失败
@@ -373,19 +418,24 @@ def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner
373
418
  if directive_block:
374
419
  prompt = directive_block + "\n\n---\n\n" + prompt
375
420
  effective_prompt = (guard["reminder"] + "\n\n---\n\n" + prompt) if guard["reminder"] else prompt
376
- res = _spawn_step(session_run_id=run_id, role=role, agent=agent,
377
- prompt=effective_prompt, workdir=workdir, readonly=readonly,
378
- ev=ev, timeout=timeout, resume=resume, step=step,
379
- log_abs=log_abs, images=images, require_tools=require_tools)
421
+ res = _spawn_step(session_run_id=run_id, role=role, agent=agent,
422
+ prompt=effective_prompt, workdir=workdir, readonly=readonly,
423
+ ev=ev, timeout=timeout, resume=resume, step=step,
424
+ log_abs=log_abs, images=images, require_tools=require_tools,
425
+ deadline=deadline)
380
426
  # 先收尾再查取消:取消时进程已被 run_process 杀停,若先抛 Cancelled,
381
- # 步骤记录会永远停在「运行中」变僵尸(与 _run_verify 的顺序对齐)
382
- _finish_step_result(run_id, step, res, role, agent, start)
383
- _check_cancel(ev)
427
+ # 步骤记录会永远停在「运行中」变僵尸(与 _run_verify 的顺序对齐)
428
+ _finish_step_result(run_id, step, res, role, agent, start)
429
+ if (res.get("raw") or {}).get("deadline_exceeded"):
430
+ store.update_run(run_id, expected_status="running", status="timeout",
431
+ ended_at=_now(), error="任务总时限已到")
432
+ raise TaskTimeout()
433
+ _check_cancel(ev)
384
434
  return res
385
435
 
386
436
 
387
- def _run_builtin_step(run_id, role, bi, prompt, workdir, ev, note="", images=None,
388
- followups=False):
437
+ def _run_builtin_step(run_id, role, bi, prompt, workdir, ev, note="", images=None,
438
+ followups=False):
389
439
  """内置智能体步骤:直连模型 API + 工具循环(builtin_agent),不经 CLI 进程。
390
440
 
391
441
  与 _run_step 对齐的三件事:暂停/取消闸门、运行中指令 drain 注入、重复调用
@@ -393,7 +443,8 @@ def _run_builtin_step(run_id, role, bi, prompt, workdir, ev, note="", images=Non
393
443
  日志只有「迭代/工具」摘要行——对话视图吃 output,日志抽屉看工具轨迹。
394
444
  followups=True 时从回答末尾解析「建议追问」块(直连对话专用协议):
395
445
  剥离出结构化列表落步骤记录,正文保持干净。"""
396
- _wait_gate(run_id, ev)
446
+ _wait_gate(run_id, ev)
447
+ deadline = _ensure_budget(run_id)
397
448
  step, log_abs = store.add_step(run_id, role, "builtin", "CodeBee", note=note,
398
449
  model=bi.get("model") or "")
399
450
  start = time.time()
@@ -437,9 +488,11 @@ def _run_builtin_step(run_id, role, bi, prompt, workdir, ev, note="", images=Non
437
488
  def _on_activity(line):
438
489
  store.stream_step(run_id, step["n"], activity=str(line or "")[:200])
439
490
 
440
- res = builtin_agent.run(bi, prompt, workdir, cancel_event=ev, log=_log, images=images,
441
- on_reason=_on_reason, on_stream=_on_stream,
442
- on_activity=_on_activity)
491
+ remaining = None if deadline is None else max(0.0, deadline - time.monotonic())
492
+ res = builtin_agent.run(bi, prompt, workdir, timeout=remaining or 180,
493
+ deadline=deadline, cancel_event=ev, log=_log, images=images,
494
+ on_reason=_on_reason, on_stream=_on_stream,
495
+ on_activity=_on_activity)
443
496
  if followups and res.get("ok"):
444
497
  clean, fups = _parse_followups(res.get("text") or "")
445
498
  if fups:
@@ -460,9 +513,13 @@ def _run_builtin_step(run_id, role, bi, prompt, workdir, ev, note="", images=Non
460
513
  res["raw"]["duration"] = time.time() - start
461
514
  res["tokens"] = int(usage.get("total") or 0)
462
515
  res.setdefault("cost_usd", 0.0)
463
- # 先收尾再查取消:与 _run_step 同序,防步骤记录停在「运行中」变僵尸
464
- _finish_step_result(run_id, step, res, role, agent_pseudo, start)
465
- _check_cancel(ev)
516
+ # 先收尾再查取消:与 _run_step 同序,防步骤记录停在「运行中」变僵尸
517
+ _finish_step_result(run_id, step, res, role, agent_pseudo, start)
518
+ if (res.get("raw") or {}).get("deadline_exceeded"):
519
+ store.update_run(run_id, expected_status="running", status="timeout",
520
+ ended_at=_now(), error="任务总时限已到")
521
+ raise TaskTimeout()
522
+ _check_cancel(ev)
466
523
  return res
467
524
 
468
525
 
@@ -485,8 +542,9 @@ def _budget_max_tokens():
485
542
  return 0
486
543
 
487
544
 
488
- def _spawn_step(session_run_id, role, agent, prompt, workdir, readonly, ev,
489
- timeout, resume, step, log_abs, images=None, require_tools=False):
545
+ def _spawn_step(session_run_id, role, agent, prompt, workdir, readonly, ev,
546
+ timeout, resume, step, log_abs, images=None, require_tools=False,
547
+ deadline=None):
490
548
  """真实 CLI 调用:压缩灰度路径或原路径。"""
491
549
  # T2.1 预算闸:已用 token 达到单次 run 上限 → 阻断后续真实调用(ENV_BLOCK)。
492
550
  # 只拦「下一步」,允许越过线的当前步完成;auto 续跑可在用户调高预算后接手。
@@ -509,10 +567,10 @@ def _spawn_step(session_run_id, role, agent, prompt, workdir, readonly, ev,
509
567
  if _compaction_enabled() and not resume:
510
568
  # Phase 2(1D):撑爆 → 压缩 → 守门重试;同时把 usage 累进 token_meter(1C)
511
569
  session = _get_session(session_run_id)
512
- llm_caller = _make_llm_caller(agent, workdir)
513
- call_kwargs = dict(workdir=workdir, readonly=readonly,
514
- timeout=timeout, cancel_event=ev, log_path=str(log_abs),
515
- images=images, require_tools=require_tools)
570
+ llm_caller = _make_llm_caller(agent, workdir, deadline=deadline)
571
+ call_kwargs = dict(workdir=workdir, readonly=readonly,
572
+ timeout=timeout, cancel_event=ev, log_path=str(log_abs),
573
+ images=images, require_tools=require_tools, deadline=deadline)
516
574
 
517
575
  def _call(p, **kw):
518
576
  nonlocal usage_recorded
@@ -537,9 +595,10 @@ def _spawn_step(session_run_id, role, agent, prompt, workdir, readonly, ev,
537
595
  session, _call, prompt, model=agent.get("model") or "",
538
596
  llm_caller=llm_caller)
539
597
  else:
540
- res = runner.run_agent(agent, prompt, workdir=workdir, readonly=readonly,
541
- timeout=timeout, cancel_event=ev, log_path=str(log_abs),
542
- resume=resume, images=images, require_tools=require_tools)
598
+ res = runner.run_agent(agent, prompt, workdir=workdir, readonly=readonly,
599
+ timeout=timeout, cancel_event=ev, log_path=str(log_abs),
600
+ resume=resume, images=images, require_tools=require_tools,
601
+ deadline=deadline)
543
602
  # 默认关闭压缩和 resume 都走直通分支,也必须把真实 usage 送进预算表;否则
544
603
  # 下一步永远看到 used=0,max_tokens_per_run 只是一个无效设置。
545
604
  if not usage_recorded:
@@ -726,14 +785,17 @@ def _files_hint(sub):
726
785
  "`%s`" % str(f) for f in files[:10])
727
786
 
728
787
 
729
- def _run_verify(run_id, task, workdir, ev):
730
- """确定性验证。返回 (verify_pass, ran)。"""
731
- if not task.get("verify_command"):
732
- return True, False
788
+ def _run_verify(run_id, task, workdir, ev):
789
+ """确定性验证。返回 (verify_pass, ran)。"""
790
+ deadline = _ensure_budget(run_id)
791
+ if not task.get("verify_command"):
792
+ return True, False
733
793
  step, log_abs = store.add_step(run_id, "verify", "builtin", "内置验证器")
734
794
  start = time.time()
735
- r = runner.run_process(shell_cmd=task["verify_command"], cwd=workdir,
736
- timeout=600, cancel_event=ev, log_path=str(log_abs))
795
+ timeout, deadline = _step_timeout(run_id, 600, deadline)
796
+ r = runner.run_process(shell_cmd=task["verify_command"], cwd=workdir,
797
+ timeout=timeout, deadline=deadline, cancel_event=ev,
798
+ log_path=str(log_abs))
737
799
  ok = r["ok"]
738
800
  # 与 _finish_step_result 同一套显示层细分:超时/取消杀停不再冒充「失败」
739
801
  if r.get("cancelled"):
@@ -743,9 +805,13 @@ def _run_verify(run_id, task, workdir, ev):
743
805
  else:
744
806
  v_status = "done" if ok else "failed"
745
807
  v_sum = "验证通过" if ok else "验证失败(exit %s)" % r["exit_code"]
746
- store.finish_step(run_id, step["n"], v_status,
747
- summary=v_sum,
748
- exit_code=r["exit_code"], duration_s=time.time() - start)
808
+ store.finish_step(run_id, step["n"], v_status,
809
+ summary=v_sum,
810
+ exit_code=r["exit_code"], duration_s=time.time() - start)
811
+ if r.get("deadline_exceeded"):
812
+ store.update_run(run_id, expected_status="running", status="timeout",
813
+ ended_at=_now(), error="任务总时限已到")
814
+ raise TaskTimeout()
749
815
  _check_cancel(ev)
750
816
  return ok, True
751
817
 
@@ -1057,11 +1123,14 @@ def _run_code(run, task, agents, ev, stats, mode):
1057
1123
  _wait_gate(run_id, ev)
1058
1124
  plan_step, plan_log = store.add_step(run_id, "plan", impl["id"], impl.get("label"),
1059
1125
  note=route.get("implementer", ""))
1060
- plan = planner.make_code_plan(_steered_task(run_id, task),
1061
- modelhub.bind_agent(impl, difficulty),
1062
- _resume_workdir(resume_ctx, workdir), ev,
1063
- resume=resume_ctx["session"] if resume_ctx else None,
1064
- log_path=str(plan_log) if plan_log else None)
1126
+ plan = _planner_call(
1127
+ planner.make_code_plan, _steered_task(run_id, task),
1128
+ modelhub.bind_agent(impl, difficulty),
1129
+ _resume_workdir(resume_ctx, workdir), ev,
1130
+ resume=resume_ctx["session"] if resume_ctx else None,
1131
+ log_path=str(plan_log) if plan_log else None,
1132
+ deadline=_run_deadline(run_id))
1133
+ _ensure_budget(run_id)
1065
1134
  # 规划器判定优先于启发式(仅当用户未显式指定难度)
1066
1135
  if not explicit and plan.get("difficulty") in ("easy", "hard"):
1067
1136
  difficulty = plan["difficulty"]
@@ -2329,8 +2398,11 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
2329
2398
  _wait_gate(run_id, ev)
2330
2399
  outline_step, outline_log = store.add_step(run_id, "outline", impl["id"], impl.get("label"),
2331
2400
  note=route.get("author", ""))
2332
- outline = planner.make_serial_outline(_steered_task(run_id, task), impl, workdir, ev,
2333
- log_path=str(outline_log) if outline_log else None)
2401
+ outline = _planner_call(
2402
+ planner.make_serial_outline, _steered_task(run_id, task), impl, workdir, ev,
2403
+ log_path=str(outline_log) if outline_log else None,
2404
+ deadline=_run_deadline(run_id))
2405
+ _ensure_budget(run_id)
2334
2406
  if outline.get("degraded") and impl.get("mode") != "mock":
2335
2407
  # 兜底模板只有章号没有情节,据此写出的两万字等于废稿——
2336
2408
  # 中止并交给自动续跑等编排者恢复后重试,而不是空转烧配额。
@@ -3500,8 +3572,11 @@ def _run_content_review(run, task, agents, ev, stats, mode):
3500
3572
  draft_note = route.get("author", "") if mode != "manual" else ""
3501
3573
 
3502
3574
  # 编排者大纲:只对真实执行有意义;失败静默退回无大纲(喂入带指令的任务副本)
3503
- outline = (planner.make_review_outline(_steered_task(run_id, task))
3504
- if workflow["outline"] and impl.get("mode") != "mock" else None)
3575
+ outline = (_planner_call(planner.make_review_outline,
3576
+ _steered_task(run_id, task),
3577
+ deadline=_run_deadline(run_id))
3578
+ if workflow["outline"] and impl.get("mode") != "mock" else None)
3579
+ _ensure_budget(run_id)
3505
3580
  if outline:
3506
3581
  workflow = task_compile.content_workflow(
3507
3582
  task, difficulty, plan=outline, mode=mode)
@@ -3989,10 +4064,14 @@ def execute_run(run_id):
3989
4064
  if store.update_run(run_id, expected_status=initial_status, status="running",
3990
4065
  started_at=_now()) is None:
3991
4066
  return
3992
- if task is None:
3993
- store.update_run(run_id, expected_status="running", status="failed",
3994
- error="找不到任务 %s" % run.get("task_id"), ended_at=_now())
3995
- return
4067
+ if task is None:
4068
+ store.update_run(run_id, expected_status="running", status="failed",
4069
+ error="找不到任务 %s" % run.get("task_id"), ended_at=_now())
4070
+ return
4071
+ try:
4072
+ _ensure_budget(run_id)
4073
+ except TaskTimeout:
4074
+ return
3996
4075
  # 统一任务编译:旧字段继续供各引擎读取,规格作为运行级诊断与调度输入落盘。
3997
4076
  task_spec = task_compile.compile_task(task)
3998
4077
  store.update_run(run_id, task_spec=task_spec,
@@ -4111,9 +4190,12 @@ def execute_run(run_id):
4111
4190
  critics, impl, route, resume_ctx, difficulty)
4112
4191
  else:
4113
4192
  _run_content_review(run, task, agents, ev, stats, mode)
4114
- except Cancelled:
4115
- store.update_run(run_id, expected_status="running",
4116
- status="cancelled", ended_at=_now())
4193
+ except TaskTimeout:
4194
+ store.update_run(run_id, expected_status="running",
4195
+ status="timeout", ended_at=_now(), error="任务总时限已到")
4196
+ except Cancelled:
4197
+ store.update_run(run_id, expected_status="running",
4198
+ status="cancelled", ended_at=_now())
4117
4199
  except Exception as e:
4118
4200
  import traceback
4119
4201
  store.update_run(run_id, expected_status="running", status="failed",
@@ -69,7 +69,7 @@ def _log_streamer(log_path, min_chars=400, min_secs=0.8):
69
69
  return cb
70
70
 
71
71
 
72
- def _outline_timeout():
72
+ def _outline_timeout():
73
73
  """大纲/规划的 CLI 兜底超时(秒):env TUTTI_OUTLINE_TIMEOUT(秒)优先,
74
74
  其次设置 orchestrator.outline_timeout_s,缺省 900。"""
75
75
  try:
@@ -86,7 +86,16 @@ def _outline_timeout():
86
86
  return v
87
87
  except Exception:
88
88
  pass
89
- return DEFAULT_OUTLINE_TIMEOUT
89
+ return DEFAULT_OUTLINE_TIMEOUT
90
+
91
+
92
+ def _deadline_timeout(deadline, requested):
93
+ if deadline is None:
94
+ return requested
95
+ try:
96
+ return max(1, min(float(requested), deadline - time.monotonic()))
97
+ except (TypeError, ValueError):
98
+ return requested
90
99
 
91
100
 
92
101
  def _log_usage(source, role, task, res, agent=None, tool="", model="", provider="",
@@ -346,7 +355,8 @@ def book_volume_plan(task, outline, upto):
346
355
  return volumes.merge_titles(plan, named)
347
356
 
348
357
 
349
- def make_serial_outline(task, author_agent=None, workdir=None, ev=None, log_path=None):
358
+ def make_serial_outline(task, author_agent=None, workdir=None, ev=None, log_path=None,
359
+ deadline=None):
350
360
  """连载大纲:编排者 API 优先 → 作者 CLI → 模板。返回 {book_title, chapters:[{title,beats,hook}]}。
351
361
 
352
362
  续写批次(serial.start_chapter > 1):改用续写大纲提示词,注入前情大纲与
@@ -443,8 +453,8 @@ def make_serial_outline(task, author_agent=None, workdir=None, ev=None, log_path
443
453
  # prompt 逐字节相同,命中省一次全量规划调用。第 2 次尝试故意
444
454
  # 旁路缓存——首试 ok 但 JSON 不合法时,重试要的是「再抽一次样
445
455
  # 本」而不是同一份坏文本。
446
- res = modelhub.chat(prov["id"], model, prompt,
447
- max_tokens=16000, timeout=300, on_delta=cb,
456
+ res = modelhub.chat(prov["id"], model, prompt,
457
+ max_tokens=16000, timeout=_deadline_timeout(deadline, 300), on_delta=cb,
448
458
  reasoning_effort=_reasoning_effort(task),
449
459
  cache_ttl=3600 if _attempt == 1 else 0)
450
460
  cb.flush()
@@ -473,7 +483,8 @@ def make_serial_outline(task, author_agent=None, workdir=None, ev=None, log_path
473
483
  _append_log(log_path, "===== 作者 CLI(%s)=====" % author_agent.get("id", "?"))
474
484
  res = runner.run_agent(modelhub.bind_agent(author_agent), prompt,
475
485
  workdir=workdir or task.get("workdir"), readonly=True,
476
- timeout=_outline_timeout(), cancel_event=ev,
486
+ timeout=_deadline_timeout(deadline, _outline_timeout()),
487
+ deadline=deadline, cancel_event=ev,
477
488
  log_path=log_path)
478
489
  _log_usage("outline", "outline", task, res, agent=author_agent)
479
490
  if not res["ok"]:
@@ -591,11 +602,13 @@ def _reasoning_effort(task):
591
602
  return "medium"
592
603
 
593
604
 
594
- def make_code_plan(task, planner_agent, workdir, ev=None, resume=None, log_path=None):
605
+ def make_code_plan(task, planner_agent, workdir, ev=None, resume=None, log_path=None,
606
+ deadline=None):
595
607
  """代码任务计划:编排者 API 优先 → CLI 智能体 → 单步模板。"""
596
608
  orch = _orchestrator()
597
609
  if orch:
598
- plan = _orch_code_plan(task, orch[0], orch[1], log_path=log_path)
610
+ plan = _orch_code_plan(task, orch[0], orch[1], log_path=log_path,
611
+ deadline=deadline)
599
612
  if plan:
600
613
  return plan
601
614
  if planner_agent is None:
@@ -607,7 +620,8 @@ def make_code_plan(task, planner_agent, workdir, ev=None, resume=None, log_path=
607
620
  .replace("__CONTEXT__", task.get("context") or "(无)")
608
621
  .replace("__VERIFY__", task.get("verify_command") or "(未配置)"))
609
622
  res = runner.run_agent(planner_agent, prompt, workdir=workdir, readonly=True,
610
- timeout=300, cancel_event=ev, resume=resume,
623
+ timeout=_deadline_timeout(deadline, 300), deadline=deadline,
624
+ cancel_event=ev, resume=resume,
611
625
  log_path=log_path)
612
626
  _log_usage("plan", "plan", task, res, agent=planner_agent)
613
627
  data = runner.extract_json(res.get("text") or "")
@@ -624,7 +638,7 @@ def _fallback_code_plan(task, note):
624
638
  "steps": [{"title": "实现任务", "detail": task["goal"]}]}
625
639
 
626
640
 
627
- def _orch_code_plan(task, prov, model, log_path=None):
641
+ def _orch_code_plan(task, prov, model, log_path=None, deadline=None):
628
642
  cb = _log_streamer(log_path)
629
643
  # §07 T2.2:代码计划同任务同 prompt(断点续跑/重试),TTL 1h 精确缓存
630
644
  res = modelhub.chat(prov["id"], model,
@@ -632,7 +646,7 @@ def _orch_code_plan(task, prov, model, log_path=None):
632
646
  .replace("__GOAL__", task["goal"])
633
647
  .replace("__CONTEXT__", task.get("context") or "(无)")
634
648
  .replace("__VERIFY__", task.get("verify_command") or "(未配置)")),
635
- max_tokens=8000, timeout=300, on_delta=cb,
649
+ max_tokens=8000, timeout=_deadline_timeout(deadline, 300), on_delta=cb,
636
650
  reasoning_effort=_reasoning_effort(task),
637
651
  cache_ttl=3600)
638
652
  cb.flush()
@@ -652,7 +666,7 @@ def _orch_code_plan(task, prov, model, log_path=None):
652
666
  "workflow": _plan_workflow(data)}
653
667
 
654
668
 
655
- def make_review_outline(task):
669
+ def make_review_outline(task, deadline=None):
656
670
  """review 类任务:编排者产出写作大纲(失败返回 None,起草退回无大纲)。"""
657
671
  orch = _orchestrator()
658
672
  if not orch:
@@ -662,7 +676,7 @@ def make_review_outline(task):
662
676
  (REVIEW_OUTLINE_PROMPT
663
677
  .replace("__GOAL__", task["goal"])
664
678
  .replace("__CONTEXT__", task.get("context") or "(无)")),
665
- max_tokens=8000, timeout=300,
679
+ max_tokens=8000, timeout=_deadline_timeout(deadline, 300),
666
680
  reasoning_effort=_reasoning_effort(task),
667
681
  cache_ttl=3600) # §07 T2.2:同任务重试幂等,TTL 1h
668
682
  _log_usage("outline", "outline", task, res, model=model,