Files
kwcode/kaiwu/core/orchestrator.py
Val-sss 2162a9585e feat: 三项框架优化 — 精准retry + ReAct首次启用 + 多文件batch
1. 精准Retry: 高通过率(差1-2个测试)时保留完整诊断不截断,
   加聚焦指令"只修这个问题不要改其他代码"(原300字符截断→800)
2. ReAct首次启用: 多文件/rename/refactor任务首次attempt就用
   ReAct循环探索,不再等retry才触发(_is_complex_task门控)
3. 多文件batch: ReactLoop增加rename专用策略指引(grep找引用→
   逐文件修改→验证), 扩展rename任务文件数限制3→8

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-14 08:09:05 +08:00

1481 lines
73 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Pipeline Orchestrator: routes Gate output to expert sequences.
RED-2: Deterministic pipeline, fixed sequence per expert_type.
RED-5: Max 3 retries, hardcoded.
"""
import logging
import time
import threading
from typing import Callable, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from kaiwu.experts.debug_subagent import DebugSubagent
from kaiwu.experts.vision_expert import VisionExpert
from kaiwu.core.context import TaskContext
from kaiwu.core.event_bus import EventBus
from kaiwu.core.cognitive_gate import CognitiveGate
from kaiwu.core.wink import WinkMonitor
from kaiwu.core.gap_detector import GapDetector, GapType, Gap, GAP_TO_EXPERT_TYPE
from kaiwu.core.execution_state import ExecutionStateTracker
from kaiwu.core.env_prober import EnvProber
from kaiwu.core.test_parser import extract_failing_tests, extract_passing_tests
from kaiwu.experts.locator import LocatorExpert
from kaiwu.experts.generator import GeneratorExpert
from kaiwu.experts.verifier import VerifierExpert
from kaiwu.experts.search_augmentor import SearchAugmentorExpert
from kaiwu.experts.search_subagent import SearchSubagent
from kaiwu.experts.office_handler import OfficeHandlerExpert
from kaiwu.experts.chat_expert import ChatExpert
from kaiwu.memory.kaiwu_md import KaiwuMemory
from kaiwu.registry.expert_registry import ExpertRegistry
from kaiwu.tools.executor import ToolExecutor
from kaiwu.flywheel.trajectory_collector import TrajectoryCollector
__all__ = ["PipelineOrchestrator"]
from kaiwu.flywheel.pattern_detector import PatternDetector
from kaiwu.flywheel.ab_tester import ABTester
from kaiwu.core.checkpoint import Checkpoint
from kaiwu.core.kwcode_md import load_kwcode_md, build_kwcode_system
from kaiwu.core.upstream_manifest import UpstreamManifest
from kaiwu.stats.value_tracker import ValueTracker
from kaiwu.notification.flywheel_notifier import FlywheelNotifier
from kaiwu.flywheel.strategy_stats import StrategyStats
from kaiwu.flywheel.user_pattern_memory import UserPatternMemory
from kaiwu.telemetry.client import TelemetryClient
from kaiwu.audit.logger import AuditLogger
from kaiwu.audit.detailed_logger import DetailedLogger
from kaiwu.core.model_capability import detect_model_tier, STRATEGIES, ModelTier
logger = logging.getLogger(__name__)
# RED-2: Fixed sequences per expert_type.
EXPERT_SEQUENCES = {
"locator_repair": ["locator", "generator", "verifier"],
"codegen": ["generator", "verifier"],
"refactor": ["locator", "generator", "verifier"],
"doc": ["locator", "generator"],
"office": ["office"],
"chat": ["chat"],
"vision": ["vision"],
}
# 错误策略路由:按 error_type 切换重试序列
# 理论来源Turn-Control StrategiesarXiv:2510.16786WinkarXiv:2602.17037
RETRY_STRATEGIES = {
"syntax": {
"sequence": ["generator", "verifier"],
"hint": "只修 {error_file}:{error_line} 的语法错误修改≤5行不触碰其他函数",
"search": False,
},
"assertion": {
"sequence": ["generator", "verifier"],
"hint": "测试期望:{error_message}。只改1个函数使断言通过修改≤10行",
"search": False,
},
"import": {
"sequence": ["import_fixer", "verifier"],
"hint": "",
"search": True,
},
"patch_apply": {
"sequence": ["locator", "generator", "verifier"],
"hint": "必须先read_file读取文件最新内容禁止使用缓存的original",
"search": False,
},
"runtime": {
"sequence": ["debugger", "generator", "verifier"],
"hint": "",
"search": False,
},
"unknown": {
"sequence": ["generator", "verifier"],
"hint": "只修改1个函数修改≤15行不触碰报错位置±20行外的代码",
"search": False,
"scope_narrow": True,
},
}
class PipelineOrchestrator:
"""Deterministic expert pipeline orchestrator."""
MAX_RETRIES = 3 # 默认值,被 _get_max_retries() 覆盖
_RETRY_BY_DIFFICULTY = {"easy": 3, "hard": 4} # 动态重试预算
def __init__(
self,
locator: LocatorExpert,
generator: GeneratorExpert,
verifier: VerifierExpert,
search_augmentor: SearchAugmentorExpert,
office_handler: OfficeHandlerExpert,
tool_executor: ToolExecutor,
memory: KaiwuMemory,
registry: Optional[ExpertRegistry] = None,
trajectory_collector: Optional[TrajectoryCollector] = None,
ab_tester: Optional[ABTester] = None,
chat_expert: Optional[ChatExpert] = None,
debug_subagent: Optional["DebugSubagent"] = None,
vision_expert: Optional["VisionExpert"] = None,
bus: Optional[EventBus] = None,
):
self.locator = locator
self.generator = generator
self.verifier = verifier
self.search_augmentor = search_augmentor
self.office_handler = office_handler
self.chat_expert = chat_expert
self.vision_expert = vision_expert
self.tools = tool_executor
self.memory = memory
self.registry = registry
self.trajectory_collector = trajectory_collector
self._pattern_detector = PatternDetector(trajectory_collector) if trajectory_collector else None
self.ab_tester = ab_tester
self.debug_subagent = debug_subagent
self._value_tracker = ValueTracker()
self._notifier = FlywheelNotifier()
self._strategy_stats = StrategyStats()
self._user_patterns = UserPatternMemory()
self._telemetry = TelemetryClient()
self._audit = AuditLogger()
# 模型能力检测从LLM后端取模型名失败默认MEDIUM
try:
model_name = getattr(self.generator.llm, 'ollama_model', '') or ''
ollama_url = getattr(self.generator.llm, 'ollama_url', 'http://localhost:11434')
self._model_tier = detect_model_tier(model_name, ollama_url)
self._model_strategy = STRATEGIES[self._model_tier]
except Exception:
self._model_tier = ModelTier.MEDIUM
self._model_strategy = STRATEGIES[ModelTier.MEDIUM]
self.bus = bus or EventBus()
self._wink = WinkMonitor()
self._cognitive_gate = CognitiveGate()
self._search_subagent = SearchSubagent(locator, tool_executor)
self._manifest = UpstreamManifest()
# MoE新增确定性基础设施
self._gap_detector = GapDetector()
self._state_tracker = ExecutionStateTracker()
self._env_prober = EnvProber()
def run(
self,
user_input: str,
gate_result: dict,
project_root: str,
on_status: "Optional[Callable[[str, str], None]]" = None,
no_search: bool = False,
skip_checkpoint: bool = False,
pre_search_results: str = "",
image_paths: Optional[list[str]] = None,
) -> dict:
"""
Execute the expert pipeline.
on_status: optional callback(stage: str, detail: str) for CLI progress display.
Returns {"success": bool, "context": TaskContext, "error": str|None, "elapsed": float}.
"""
start_time = time.time()
self._audit.start()
# ── DetailedLogger完整流水线日志 ──
model_name = getattr(self, '_model_name', 'unknown')
self._detailed = DetailedLogger(user_input=user_input, model=model_name)
self._detailed.set_metadata(
project_root=project_root,
gate_result=gate_result,
)
# 设置 LLM 回调,自动记录每次调用
if self._detailed.enabled:
def _llm_hook(messages, raw_output, elapsed_ms):
self._detailed.log_llm(
caller="llm_backend",
prompt="",
system="",
raw_output=raw_output,
elapsed_ms=elapsed_ms,
messages=messages,
)
llm = getattr(self.generator, 'llm', None)
if llm and hasattr(llm, '_on_llm_call'):
llm._on_llm_call = _llm_hook
# 任务级超时看门狗
TASK_TIMEOUT_S = 600 # 单任务最长10分钟32B模型3次采样+execution_feedback需要更多时间
_watchdog_triggered = threading.Event()
def _watchdog_timer():
_watchdog_triggered.set()
_watchdog = threading.Timer(TASK_TIMEOUT_S, _watchdog_timer)
_watchdog.daemon = True
_watchdog.start()
# 保存 project_root 供 Gate 2 回测使用
self._backtest_project_root = project_root
ctx = TaskContext(
user_input=user_input,
project_root=project_root,
gate_result=gate_result,
kaiwu_memory=self.memory.load(project_root),
expert_system_prompt=gate_result.get("system_prompt", ""),
)
# 注入审计日志引用让Generator等专家能记录LLM调用
ctx._audit_logger = self._audit
# 注入 DetailedLogger 引用到 ctx让 Generator 等专家能直接写入
ctx._detailed_logger = self._detailed
# 模型能力等级注入ctx
ctx.model_tier = self._model_tier.value
try:
ctx.effective_ctx = getattr(self.generator.llm, '_effective_ctx', 32768)
except AttributeError:
ctx.effective_ctx = 32768
# 用户错误模式提示注入
warning = self._user_patterns.get_warning_hint()
if warning:
ctx.kaiwu_memory = (ctx.kaiwu_memory + "\n\n" + warning).strip()
# 错误类型追踪
ctx._errors_encountered = []
# 详细attempt记录供trajectory_collector使用
ctx._trajectory_attempts = []
# 每次顶层任务重置manifest
self._manifest.clear()
# 处理图片路径
if image_paths:
ctx.image_paths = list(image_paths)
logger.info(f"[orchestrator] 任务包含 {len(image_paths)} 张图片")
expert_type = gate_result.get("expert_type", "locator_repair")
difficulty = gate_result.get("difficulty", "medium")
# AdaptThink: 根据任务类型×难度设置think预算
from kaiwu.core.think_config import get_think_config
ctx.think_config = get_think_config(expert_type, difficulty)
# 预搜索结果注入
if pre_search_results:
ctx.search_results = pre_search_results
ctx.search_triggered = True
self._emit(on_status, "search", "已预加载实时数据")
# KWCODE.md 规则注入
kwcode_sections = load_kwcode_md(project_root)
if kwcode_sections:
kwcode_rules = build_kwcode_system(expert_type, kwcode_sections)
if kwcode_rules:
ctx.kwcode_rules = kwcode_rules
# 追加到 expert_system_prompt使规则流向所有专家
if ctx.expert_system_prompt:
ctx.expert_system_prompt = f"{kwcode_rules}\n\n{ctx.expert_system_prompt}"
else:
ctx.expert_system_prompt = kwcode_rules
# chat/vision类型早期返回
simple_result = self._handle_simple_type(ctx, expert_type, start_time, on_status)
if simple_result is not None:
return simple_result
# ══════════════════════════════════════
# Phase 0环境探针确定性不走LLM
# ══════════════════════════════════════
self._emit(on_status, "env_probe", "检测项目环境...")
try:
env_result = self._env_prober.probe_and_fix(project_root, self.tools)
if env_result.get("installed"):
self._emit(on_status, "env_setup",
f"已安装:{', '.join(env_result['installed'])}")
if env_result.get("test_cmd"):
ctx.confirmed_test_cmd = env_result["test_cmd"]
except Exception as e:
logger.debug("EnvProber failed (non-blocking): %s", e)
# ══════════════════════════════════════
# Phase 1前置测试 + GapDetector无条件非chat/vision/office
# ══════════════════════════════════════
self._state_tracker.reset()
if expert_type not in ("chat", "vision", "office"):
try:
self._emit(on_status, "pre_test", "运行初始测试获取基线...")
pre_result = self.verifier.run_tests_only(ctx)
pre_output = pre_result.get("output", "")
if pre_output:
ctx.initial_test_failure = pre_output
# 记录初始通过数供verifier判断是否退步
ctx._pre_test_passed = pre_result.get("passed", 0)
# GapDetector计算初始Gap驱动后续所有决策
ctx.gap = self._gap_detector.compute(pre_output, project_root)
# 状态追踪器设置基线
failing_tests = extract_failing_tests(pre_output)
self._state_tracker.set_baseline(failing_tests)
self._emit(on_status, "pre_test_done",
f"初始Gap{ctx.gap.gap_type.value}(置信度{ctx.gap.confidence:.0%}")
# Gap驱动的expert_type覆盖确定性优先于LLM分类
if ctx.gap.gap_type != GapType.UNKNOWN and ctx.gap.confidence >= 0.7:
gap_expert = GAP_TO_EXPERT_TYPE.get(ctx.gap.gap_type)
if gap_expert and gap_expert != expert_type:
self._emit(on_status, "gap_override",
f"Gap路由覆盖{expert_type}{gap_expert}")
expert_type = gap_expert
gate_result = {**gate_result, "expert_type": expert_type}
ctx.routing_source = "gap_detector"
else:
ctx.routing_source = gate_result.get("routing_source", "keyword")
else:
ctx.routing_source = gate_result.get("routing_source", "keyword")
except Exception as e:
logger.debug("Pre-test/GapDetector failed (non-blocking): %s", e)
ctx.routing_source = gate_result.get("routing_source", "keyword")
# Gate 3: AB测试
ab_candidate_name, ab_used_new, gate_result = self._setup_ab_test(gate_result, expert_type, on_status)
# MISSING_TOOLCHAIN快速熔断工具链缺失时不进retry循环直接告知用户
if ctx.gap and ctx.gap.gap_type == GapType.MISSING_TOOLCHAIN:
self._emit(on_status, "circuit_break",
f"工具链缺失:{ctx.gap.error_msg[:100]},请手动安装后重试")
self.bus.emit("circuit_break", {"msg": "missing_toolchain"})
elapsed = time.time() - start_time
_watchdog.cancel()
checkpoint = Checkpoint(project_root)
return self._record_failure_result(ctx, project_root, gate_result,
None, False, 0,
elapsed, checkpoint, False, on_status)
# 优先使用专家注册表的自定义pipeline否则用默认
if gate_result.get("route_type") == "expert_registry" and "pipeline" in gate_result:
sequence = gate_result["pipeline"]
else:
sequence = EXPERT_SEQUENCES.get(expert_type, ["generator", "verifier"])
self._emit(on_status, "gate", f"任务类型:{expert_type} | 难度:{gate_result.get('difficulty', '?')} | 路由:{ctx.routing_source}")
# DetailedLogger: gate 决策
self._detailed.log_node("gate",
{"user_input": user_input[:500]},
{"expert_type": expert_type, "difficulty": gate_result.get("difficulty", "?"), "routing_source": ctx.routing_source, "confidence": gate_result.get("confidence", 0), "sequence": sequence})
# ── MoE专家选择已移除独立Expert类统一走pipeline ──
moe_expert = self._select_moe_expert(ctx, expert_type)
# 经验回放 + 预搜索 + 计划生成跳过已被MoE专家处理的场景
if not moe_expert:
self._prepare_context(ctx, gate_result, expert_type, user_input, project_root, no_search, on_status)
# 检查点:执行前快照(多任务时跳过,避免竞态)
checkpoint = Checkpoint(project_root)
checkpoint_saved = False
if not skip_checkpoint:
checkpoint_saved = checkpoint.save()
if not checkpoint_saved:
self._emit(on_status, "warning", "无法创建文件快照,任务失败时需手动还原")
# 按任务难度动态调整重试预算
max_retries = self._get_max_retries(gate_result)
# CognitiveGate 重置
self._cognitive_gate.reset()
while ctx.retry_count < max_retries:
# 看门狗检查:超时则中止
if _watchdog_triggered.is_set():
self._emit(on_status, "watchdog", f"任务超时({TASK_TIMEOUT_S}s),强制终止")
self.bus.emit("circuit_break", {"msg": f"任务超时({TASK_TIMEOUT_S}s)"})
break
success = self._run_sequence(sequence, ctx, on_status)
# 通知 locator 任务结果(图统计 + 增量更新)
self._notify_locator(ctx, success)
if success:
elapsed = time.time() - start_time
result = self._record_success(ctx, project_root, gate_result,
ab_candidate_name, ab_used_new, elapsed,
checkpoint, on_status)
if result is not None:
return result
# Reviewer审查不通过 → 当作失败,继续重试
self._emit(on_status, "review_retry", "审查不通过,重试修改...")
# Reset for retry保留locator_output只重新生成
ctx.generator_output = None
ctx.verifier_output = None
ctx.retry_count += 1
error_detail = ""
if ctx.verifier_output:
error_detail = ctx.verifier_output.get("error_detail", "")
# syntax error不消耗retry次数LLM输出格式问题不是逻辑问题
# 但限制最多2次syntax免费重试防止无限循环
if ctx.verifier_output and ctx.verifier_output.get("error_type") == "syntax":
if not hasattr(ctx, '_syntax_free_retries'):
ctx._syntax_free_retries = 0
ctx._syntax_free_retries += 1
if ctx._syntax_free_retries <= 2:
ctx.retry_count -= 1 # 退回,不算这次
# 保存失败信息用于重试策略
ctx.previous_failure = error_detail
# CognitiveGate: 检测边际收益递减
if ctx.generator_output:
self._cognitive_gate.record(ctx.generator_output.get("patches", []))
cg_stop, cg_reason = self._cognitive_gate.should_stop()
if cg_stop:
self._emit(on_status, "circuit_break", cg_reason)
self.bus.emit("circuit_break", {"msg": cg_reason})
break
# Circuit breaker: same error_type streak
current_error_type = ""
if ctx.verifier_output:
current_error_type = ctx.verifier_output.get("error_type", "unknown")
# 追踪错误类型用于飞轮统计
if current_error_type:
ctx._errors_encountered.append(current_error_type)
if not hasattr(ctx, '_error_type_streak'):
ctx._error_type_streak = {"type": "", "count": 0}
if current_error_type and current_error_type == ctx._error_type_streak["type"]:
ctx._error_type_streak["count"] += 1
else:
ctx._error_type_streak = {"type": current_error_type, "count": 1}
# ── TraceCoder: 累积历史教训(不重置) ──
attempt_record = {
"attempt": ctx.retry_count,
"error_type": current_error_type,
"passed_tests": extract_passing_tests(error_detail) if error_detail else [],
"failed_tests": (ctx.verifier_output or {}).get("failed_tests", []),
"patch_files": [p.get("file", "") for p in (ctx.generator_output or {}).get("patches", [])],
"error_message": (ctx.verifier_output or {}).get("error_message", "")[:200],
}
ctx.attempt_history.append(attempt_record)
# ── Trajectory详细attempt记录诊断用 ──
try:
_traj_attempt = self._build_trajectory_attempt(ctx, current_error_type)
ctx._trajectory_attempts.append(_traj_attempt)
except Exception:
pass # 记录失败不影响主流程
# 快速熔断:语法错误重试无效
# syntax熔断按tier区分SMALL立刻熔断MEDIUM/LARGE多给一次
_syntax_max = 1 if self._model_tier == ModelTier.SMALL else 2
if current_error_type == "syntax" and ctx.retry_count >= _syntax_max:
self._emit(on_status, "circuit_break", "语法错误重试无效,模型能力不足以完成此任务")
self.bus.emit("circuit_break", {"msg": "syntax error"})
break
# Fast circuit break: missing imports — try import_fixer first
if current_error_type == "import":
fixed = self._try_import_fix(ctx, on_status)
if not fixed:
# 尝试创建缺失的本地模块用LLM生成内容
error_msg = ctx.verifier_output.get("error_message", "") if ctx.verifier_output else ""
error_detail = ctx.verifier_output.get("error_detail", "") if ctx.verifier_output else ""
self.generator._maybe_create_missing_module(ctx, error_detail or error_msg)
# 检查是否创建成功(文件是否存在了)
import re as _re
mod_match = _re.search(r"No module named '(\w+)'", error_msg)
if mod_match:
import os
mod_file = os.path.join(ctx.project_root, f"{mod_match.group(1)}.py")
if os.path.exists(mod_file):
# 模块已创建,继续重试
self._emit(on_status, "create_module",
f"已创建缺失模块: {mod_match.group(1)}.py")
else:
missing = error_msg
self._emit(on_status, "circuit_break", f"缺少依赖:{missing},请先安装")
self.bus.emit("circuit_break", {"msg": f"import: {missing}"})
break
else:
missing = error_msg
self._emit(on_status, "circuit_break", f"缺少依赖:{missing},请先安装")
self.bus.emit("circuit_break", {"msg": f"import: {missing}"})
break
# import修复成功继续重试
# 硬熔断同类错误连续3次
if ctx._error_type_streak["count"] >= 3:
self._emit(on_status, "circuit_break",
f"同类错误({current_error_type})连续{ctx._error_type_streak['count']}次,停止重试")
self.bus.emit("circuit_break", {"msg": f"{current_error_type} x{ctx._error_type_streak['count']}"})
break
# Wink 自修复:检测偏离并注入纠正
wink_hint = self._wink.check(ctx, self.bus)
# 错误策略路由:按 error_type 切换重试序列
# contract_violation走patch_apply策略重新定位+重新生成)
if current_error_type == "contract_violation":
current_error_type = "patch_apply" # Re-locate to get fresh context
retry_strategy = RETRY_STRATEGIES.get(current_error_type, RETRY_STRATEGIES["unknown"])
sequence = retry_strategy["sequence"]
ctx.retry_hint = self._build_retry_hint(ctx, current_error_type)
if wink_hint:
ctx.retry_hint = (ctx.retry_hint + "\n" + wink_hint).strip() if ctx.retry_hint else wink_hint
self._emit(on_status, "retry", f"{ctx.retry_count}次尝试失败:{error_detail[:100]}")
self.bus.emit("retry", {"count": ctx.retry_count, "error": error_detail[:100]})
# DetailedLogger: 记录重试决策
self._detailed.log_decision("retry",
decision=f"retry #{ctx.retry_count}, strategy={retry_strategy.get('sequence', [])}",
reason=f"error_type={current_error_type}, detail={error_detail[:200]}",
context={"error_type_streak": ctx._error_type_streak, "attempt_record": attempt_record})
# 设置重试策略:每次重试用不同方法
ctx.retry_strategy = ctx.retry_count # 0→1→2
# ── ExecutionStateTracker记录测试状态变化 ──
if ctx.verifier_output:
test_output = ctx.verifier_output.get("error_detail", "")
current_failing = extract_failing_tests(test_output)
current_passing = extract_passing_tests(test_output)
new_gap = self._gap_detector.compute(test_output, project_root)
self._state_tracker.record(
attempt=ctx.retry_count,
current_failing=current_failing,
current_passing=current_passing,
gap_type=new_gap.gap_type.value,
)
# 回归检测:新引入的失败 → 回滚
if self._state_tracker.has_regression():
self._emit(on_status, "regression_detected",
f"{ctx.retry_count}次修改引入了回归,回滚...")
# 优先用regression_guard的best_code_snapshot回滚保留部分进展
# 只有在没有best_code_snapshot时才用checkpoint完全回滚到原始状态
if ctx.best_code_snapshot:
for fname, content in ctx.best_code_snapshot.items():
try:
self.tools.write_file(fname, content)
except Exception:
pass
self._emit(on_status, "regression_rollback",
f"已回滚到最优状态({ctx.best_tests_passed}个测试通过)")
elif checkpoint_saved:
checkpoint.restore()
# TraceCoder: 回归时携带具体信息,不是空白重试
regressed_tests = self._state_tracker.get_new_failures() if hasattr(self._state_tracker, 'get_new_failures') else []
ctx.retry_hint = (
f"上次修改引入了回归(新增失败:{', '.join(regressed_tests[:3]) if regressed_tests else '未知'})。\n"
f"请保留已通过测试的修复,只修改导致回归的部分。"
)
# 更新gap驱动下一轮
ctx.gap = new_gap
# env_changed处理EnvProber安装依赖后返回
if ctx.generator_output and ctx.generator_output.get("env_changed"):
ctx.gap = self._recompute_gap(ctx, project_root)
self._emit(on_status, "env_changed", "环境已变化,重新分析...")
# Reset expert outputs for retry (RED-3: fresh context each attempt)
# 保留locator_output如果下一轮sequence不含locatorgenerator需要它
# 只有sequence包含locator时才清空locator会重新填充
if "locator" in sequence:
ctx.locator_output = None
ctx.relevant_code_snippets = {}
ctx.generator_output = None
ctx.verifier_output = None
# 清除临时调试信息防止context污染
ctx.debug_info = ""
_watchdog.cancel() # Clean up watchdog timer
elapsed = time.time() - start_time
# ── 不退步保护闭环:任务失败时把最优中间状态写回磁盘 ──
# 不受KWCODE_NO_RESTORE影响这是主动保留最优不是还原checkpoint
if ctx.best_tests_passed > 0 and ctx.best_code_snapshot:
for fname, content in ctx.best_code_snapshot.items():
try:
self.tools.write_file(fname, content)
except Exception:
pass
self._emit(on_status, "best_state_restored",
f"最终保留最优状态:{ctx.best_tests_passed}个测试通过")
return self._record_failure_result(ctx, project_root, gate_result,
ab_candidate_name, ab_used_new, max_retries,
elapsed, checkpoint, checkpoint_saved, on_status)
def _handle_simple_type(self, ctx: TaskContext, expert_type: str, start_time: float, on_status) -> Optional[dict]:
"""Handle chat and vision early returns. Returns result dict or None to continue."""
# chat类型直接回复不走AB测试/搜索/重试
if expert_type == "chat":
self._emit(on_status, "chat", "聊天模式")
if self.chat_expert:
result = self.chat_expert.run(ctx)
else:
ctx.generator_output = {"explanation": "我是KWCode专注于代码任务。", "patches": []}
result = {"passed": True}
elapsed = time.time() - start_time
self._detailed.log_node("chat", {"user_input": ctx.user_input[:500]}, {"explanation": (ctx.generator_output or {}).get("explanation", "")[:500]})
self._detailed.write(expert_type="chat", success=True)
return {
"success": True,
"context": ctx,
"error": None,
"elapsed": elapsed,
}
# vision类型图片处理任务
if expert_type == "vision":
self._emit(on_status, "vision", "图片处理模式")
if self.vision_expert and ctx.image_paths:
result = self.vision_expert.run(ctx)
explanation = result.get("output", "").strip()
ctx.generator_output = {
"explanation": explanation,
"patches": [],
"metadata": {"vision": result.get("metadata", {})},
}
elapsed = time.time() - start_time
success = result.get("success", False)
return {
"success": success,
"context": ctx,
"error": None if success else explanation or "图片处理失败",
"elapsed": elapsed,
}
else:
ctx.generator_output = {"explanation": "图片处理功能需要配置Vision专家", "patches": []}
elapsed = time.time() - start_time
return {
"success": False,
"context": ctx,
"error": "Vision专家未配置或未提供图片",
"elapsed": elapsed,
}
return None
def _setup_ab_test(self, gate_result: dict, expert_type: str, on_status) -> tuple:
"""Setup AB test. Returns (ab_candidate_name, ab_used_new, gate_result)."""
ab_candidate_name = None
ab_used_new = False
if self.ab_tester and expert_type != "chat":
candidate_def = self.ab_tester.should_use_candidate(expert_type)
if candidate_def:
ab_candidate_name = candidate_def["name"]
ab_used_new = True
# 覆盖gate_result使用候选专家流水线
gate_result = {
**gate_result,
"expert_name": ab_candidate_name,
"route_type": "expert_registry",
"pipeline": candidate_def.get("pipeline", []),
"system_prompt": candidate_def.get("system_prompt", ""),
}
self._emit(on_status, "ab_test", f"AB测试使用候选专家 {ab_candidate_name}")
else:
# 检查是否有候选专家在AB测试中基线对照
for name, info in self.ab_tester._candidates.items():
if (info["status"] == "ab_testing"
and info["expert_def"].get("type") == expert_type
and len(info["ab_results"]) < 10):
ab_candidate_name = name
ab_used_new = False
self._emit(on_status, "ab_test", f"AB测试基线对照候选 {name}")
break
return (ab_candidate_name, ab_used_new, gate_result)
def _prepare_context(self, ctx: TaskContext, gate_result: dict, expert_type: str,
user_input: str, project_root: str, no_search: bool, on_status) -> None:
"""Experience replay + pre-search + plan generation."""
# Experience Replay: find similar successful trajectories
if self.trajectory_collector and expert_type not in ("chat", "office", "vision"):
try:
similar = self.trajectory_collector.find_similar(user_input, expert_type, k=3)
if similar:
ctx.similar_trajectories = similar
best = similar[0]
self._emit(on_status, "replay",
f"发现相似成功案例:{best.get('user_input', '')[:40]}")
except Exception as e:
logger.debug("Experience replay failed (non-blocking): %s", e)
# codegen任务如果涉及实时数据首次就触发搜索不等失败重试
if expert_type == "codegen" and not no_search and self._needs_realtime_data(user_input):
try:
self._emit(on_status, "search", "检测到实时数据需求,预搜索...")
results = self.search_augmentor.search(ctx)
if results:
ctx.search_results = results
ctx.search_triggered = True
self._emit(on_status, "search_done", f"搜索完成,注入{len(results)}字参考信息")
except Exception as e:
logger.debug("Pre-search failed (网络保护,不阻塞): %s", e)
# Plan 自动触发hard 任务自动生成计划(不打断用户)
if (gate_result.get("difficulty") == "hard"
and expert_type not in ("chat", "office", "vision")
and not ctx.subtask_results):
try:
from kaiwu.core.planner import Planner
from kaiwu.memory import pattern_md
planner = Planner(
locator=self.locator,
pattern_md_module=pattern_md,
llm=self.generator.llm,
)
plan = planner.generate_plan_steps(user_input, gate_result, project_root)
if plan and len(plan) > 1:
ctx.execution_plan = plan
self._emit(on_status, "plan_generated", f"自动生成 {len(plan)} 步计划")
self.bus.emit("plan_generated", {"steps": len(plan), "msg": f"生成 {len(plan)} 步计划"})
except Exception as e:
logger.debug("Auto-plan failed (non-blocking): %s", e)
def _record_success(self, ctx: TaskContext, project_root: str, gate_result: dict,
ab_candidate_name, ab_used_new: bool, elapsed: float,
checkpoint, on_status) -> dict:
"""Record success: memory, registry, trajectory, AB, value, milestone, reflection."""
# Reviewer: 需求对齐审查 — 测试全部通过或无测试结果时跳过无证据时Reviewer会幻觉
v = ctx.verifier_output or {}
tests_passed = v.get("tests_passed", 0)
tests_total = v.get("tests_total", 0)
skip_review = (tests_total > 0 and tests_passed == tests_total) or tests_total == 0
if not skip_review:
# 测试没全通过才需要Reviewer审查
review_result = self._do_review(ctx, on_status)
if review_result and not review_result.get("aligned") and review_result.get("confidence", 0) >= 0.7:
gap = review_result.get("gap", "")
self._emit(on_status, "review_reject", f"审查不通过:{gap}")
ctx.retry_hint = f"改错了:{gap}"
ctx.locator_output = None # 强制重新定位修复
return None # 返回None信号给retry loop
checkpoint.discard() # 审查通过才清理快照
# 成功时保存记忆(含耗时,用于专家/模式追踪)
self.memory.save(project_root, ctx, elapsed=elapsed)
# 更新专家注册表统计
expert_name = gate_result.get("expert_name")
if expert_name and self.registry:
self.registry.update_stats(expert_name, success=True, latency=elapsed)
# 飞轮:记录轨迹+检测模式(非阻塞)
self._record_trajectory(ctx, True, elapsed, on_status)
# 记录AB测试结果
self._record_ab_result(ab_candidate_name, ab_used_new, True, elapsed, on_status)
# 价值追踪本地SQLite
self._record_value(project_root, gate_result, True, elapsed, ctx)
# 里程碑检查
self._check_milestone(on_status)
# Reflexion持久化成功时也记录注意事项
self._persist_reflection(project_root, ctx, gate_result, success=True)
# 飞轮:策略统计 + 用户模式 + 遥测
self._record_flywheel(ctx, gate_result, True)
# 审计日志
self._audit.write(ctx, elapsed, True, getattr(self, '_model_name', 'unknown'))
# DetailedLogger: 写入成功日志
self._detailed.write(expert_type=gate_result.get("expert_type", "unknown"), success=True)
return {
"success": True,
"context": ctx,
"error": None,
"elapsed": elapsed,
}
def _record_failure_result(self, ctx: TaskContext, project_root: str, gate_result: dict,
ab_candidate_name, ab_used_new: bool, max_retries: int,
elapsed: float, checkpoint, checkpoint_saved: bool,
on_status) -> dict:
"""Record failure: checkpoint restore, memory, registry, trajectory, AB, value, reflection."""
# Checkpoint: restore on failure — 但如果有best_code_snapshot部分进展保留进展不恢复
if checkpoint_saved and not ctx.best_code_snapshot:
restored = checkpoint.restore()
if restored:
self._emit(on_status, "checkpoint", "已还原到任务执行前的状态")
else:
self._emit(on_status, "warning", "还原失败,请手动检查文件")
elif ctx.best_code_snapshot:
# 有部分进展重新写回best_state确保不被其他逻辑覆盖
for fname, content in ctx.best_code_snapshot.items():
try:
self.tools.write_file(fname, content)
except Exception:
pass
self._emit(on_status, "best_state_kept",
f"保留最优中间状态({ctx.best_tests_passed}个测试通过)")
# 降级建议
self._suggest_downgrade(ctx, on_status)
# 记录失败到模式记忆
self.memory.save_failure(project_root, ctx, elapsed=elapsed)
# 失败时更新专家统计
expert_name = gate_result.get("expert_name")
if expert_name and self.registry:
self.registry.update_stats(expert_name, success=False, latency=elapsed)
# 飞轮:记录失败轨迹
self._record_trajectory(ctx, False, elapsed, on_status)
# 记录AB测试失败
self._record_ab_result(ab_candidate_name, ab_used_new, False, elapsed, on_status)
# P2: Value tracking (local SQLite)
self._record_value(project_root, gate_result, False, elapsed, ctx)
# Reflexion持久化失败时记录根因
self._persist_reflection(project_root, ctx, gate_result, success=False)
# 飞轮:策略统计 + 用户模式 + 遥测
self._record_flywheel(ctx, gate_result, False)
# 审计日志
self._audit.write(ctx, elapsed, False, getattr(self, '_model_name', 'unknown'))
# DetailedLogger: 写入失败日志
self._detailed.write(expert_type=gate_result.get("expert_type", "unknown"), success=False)
return {
"success": False,
"context": ctx,
"error": f"Max retries ({max_retries}) exceeded",
"elapsed": elapsed,
}
def _run_sequence(self, sequence: list[str], ctx: TaskContext, on_status) -> bool:
"""Execute a fixed expert sequence. Returns True if all steps pass."""
for step in sequence:
if step == "locator":
self._emit(on_status, "locator", "定位中(隔离搜索)...")
# 使用SearchSubagent隔离context并行读取
search_result = self._search_subagent.search(ctx, self._manifest)
if not search_result or not search_result.get("relevant_files"):
self._emit(on_status, "locator_fail", "定位失败")
self._detailed.log_node("locator", {"user_input": ctx.user_input[:200]}, {"result": None}, detail="定位失败")
return False
# 将干净结果传给ctxGenerator只看到这些
ctx.locator_output = {
"relevant_files": search_result["relevant_files"],
"relevant_functions": search_result["relevant_functions"],
"edit_locations": search_result["edit_locations"],
"method": search_result["method"],
}
ctx.relevant_code_snippets = search_result["code_snippets"]
# 注入跨文件契约给Generator
if search_result.get("upstream_constraints"):
ctx.upstream_constraints = search_result["upstream_constraints"]
files = search_result["relevant_files"]
funcs = search_result["relevant_functions"]
func_str = ', '.join(funcs[:3]) if funcs else "(文件级修改)"
self._emit(on_status, "locator_done", f"文件:{', '.join(files[:3])} | 函数:{func_str}")
# DetailedLogger: locator 节点
self._detailed.log_node("locator",
{"user_input": ctx.user_input[:200], "gap_type": str(ctx.gap.gap_type.value) if ctx.gap and hasattr(ctx.gap, 'gap_type') else ""},
{"files": files, "functions": funcs, "method": search_result.get("method", ""), "upstream_constraints": search_result.get("upstream_constraints", "")[:500]})
elif step == "generator":
self._emit(on_status, "generator", "生成patch...")
result = self.generator.run(ctx)
if not result:
self._emit(on_status, "generator_fail", "生成失败")
self._detailed.log_node("generator", {"files": (ctx.locator_output or {}).get("relevant_files", [])}, {"result": None}, detail="生成失败")
return False
n_patches = len(result.get("patches", []))
self._emit(on_status, "generator_done", f"生成{n_patches}个patch")
# 用新patch更新manifest跨文件追踪
self._manifest.update(result.get("patches", []))
# DetailedLogger: generator 节点
self._detailed.log_node("generator",
{"files": (ctx.locator_output or {}).get("relevant_files", []), "functions": (ctx.locator_output or {}).get("relevant_functions", [])},
{"patch_count": n_patches, "files_modified": [p.get("file", "") for p in result.get("patches", [])], "explanation": result.get("explanation", "")})
elif step == "verifier":
self._emit(on_status, "verifier", "验证中...")
# 运行测试前做跨文件一致性检查
contract_violations = self._check_contracts(ctx)
if contract_violations:
detail = "; ".join(contract_violations[:3])
self._emit(on_status, "contract_violation", f"跨文件契约冲突:{detail[:100]}")
ctx.verifier_output = {
"passed": False,
"syntax_ok": True,
"tests_passed": 0,
"tests_total": 0,
"error_detail": f"Contract violations: {detail}",
"error_type": "contract_violation",
"error_file": "",
"error_line": 0,
"error_message": detail[:200],
"failed_tests": [],
}
self._detailed.log_node("verifier", {"patch_count": len((ctx.generator_output or {}).get("patches", []))}, {"passed": False, "error_type": "contract_violation", "detail": detail[:300]})
return False
result = self.verifier.run(ctx)
if not result or not result.get("passed"):
detail = result.get("error_detail", "unknown") if result else "no result"
self._emit(on_status, "verifier_fail", f"验证失败:{detail[:80]}")
self._detailed.log_node("verifier",
{"patch_count": len((ctx.generator_output or {}).get("patches", []))},
{"passed": False, "error_type": result.get("error_type", "") if result else "", "error_detail": detail[:1000], "tests_passed": result.get("tests_passed", 0) if result else 0, "tests_total": result.get("tests_total", 0) if result else 0})
# ── 不退步保护verifier失败时检查是否退步 ──
self._regression_guard(ctx, result, on_status)
# 额外保障从patch内容直接保存最优状态verifier已rollback磁盘文件
tp_fail = result.get("tests_passed", 0) if result else 0
if tp_fail > ctx.best_tests_passed and ctx.generator_output:
ctx.best_tests_passed = tp_fail
snapshot = {}
for p in ctx.generator_output.get("patches", []):
f = p.get("file", "")
if not f:
continue
# 从patch内容获取verifier已rollback磁盘不能从磁盘读
if p.get("write_mode") == "whole_file":
content = p.get("content", "") or p.get("modified", "")
elif p.get("modified"):
# apply_patch模式需要从original+modified重建
orig = p.get("original", "")
mod = p.get("modified", "")
full = self.tools.read_file(f) # 已rollback的原始文件
if full and not full.startswith("[ERROR]") and orig:
content = full.replace(orig, mod, 1)
else:
content = ""
else:
content = ""
if content:
snapshot[f] = content
if snapshot:
ctx.best_code_snapshot = snapshot
return False
tp = result.get("tests_passed", 0)
tt = result.get("tests_total", 0)
self._emit(on_status, "verifier_done", f"语法OK | 测试:{tp}/{tt}")
self._detailed.log_node("verifier",
{"patch_count": len((ctx.generator_output or {}).get("patches", []))},
{"passed": True, "tests_passed": tp, "tests_total": tt})
# ── 不退步保护:成功时更新最优快照 ──
self._regression_guard(ctx, result, on_status)
elif step == "office":
self._emit(on_status, "office", "生成Office文档...")
result = self.office_handler.run(ctx)
if not result.get("passed", False):
self._emit(on_status, "office_fail", result.get("error", "生成失败"))
return False
self._emit(on_status, "office_done", result.get("output", "完成"))
return True
def _check_contracts(self, ctx: TaskContext) -> list[str]:
"""Check cross-file contract consistency using UpstreamManifest. Zero LLM."""
if not ctx.generator_output:
return []
patches = ctx.generator_output.get("patches", [])
violations = []
for patch in patches:
file_path = patch.get("file", "")
modified = patch.get("modified", "")
if file_path and modified:
v = self._manifest.check_consistency(file_path, modified)
violations.extend(v)
return violations
def _record_trajectory(self, ctx: TaskContext, success: bool, elapsed: float, on_status):
"""Record trajectory and run pattern detection (non-blocking, never raises)."""
if not self.trajectory_collector:
return
try:
model = getattr(self, '_model_name', 'unknown')
self.trajectory_collector.record(ctx, success, elapsed, model)
# 成功时检查飞轮候选
if success and self._pattern_detector:
candidates = self._pattern_detector.detect()
if candidates:
names = [c["expert_type"] for c in candidates]
self._emit(on_status, "flywheel", f"发现{len(candidates)}个专家候选:{names}")
except Exception as e:
logger.debug("Flywheel recording failed (non-blocking): %s", e)
def _record_ab_result(self, candidate_name, used_new, success, elapsed, on_status):
"""Record AB test result for gate 3 (non-blocking, never raises)."""
if not self.ab_tester or not candidate_name:
return
try:
self.ab_tester.record_ab_result(candidate_name, used_new, success, elapsed)
total = len(self.ab_tester._candidates.get(candidate_name, {}).get("ab_results", []))
self._emit(on_status, "ab_test_record",
f"AB结果已记录{'候选' if used_new else '基线'} "
f"{'成功' if success else '失败'} ({total}/10)")
# 自动毕业在record_ab_result内处理总数>=10时
status = self.ab_tester._candidates.get(candidate_name, {}).get("status", "")
if status == "graduated":
self._emit(on_status, "ab_graduated",
f"专家 {candidate_name} 通过Gate 3已注册投产")
elif status == "archived":
self._emit(on_status, "ab_archived",
f"专家 {candidate_name} 未通过Gate 3已归档")
except Exception as e:
logger.debug("AB result recording failed (non-blocking): %s", e)
def _notify_locator(self, ctx: TaskContext, success: bool):
"""Notify locator of task result for graph stats + incremental update (non-blocking)."""
try:
if hasattr(self.locator, 'notify_task_result'):
self.locator.notify_task_result(ctx, success)
except Exception as e:
logger.debug("Locator notify failed (non-blocking): %s", e)
def _suggest_downgrade(self, ctx: TaskContext, on_status):
"""Post-failure: suggest narrowing scope (small model enhancement)."""
files = ctx.locator_output.get("relevant_files", []) if ctx.locator_output else []
functions = ctx.locator_output.get("relevant_functions", []) if ctx.locator_output else []
if len(files) > 1 and functions:
first_func = functions[0]
self._emit(on_status, "suggest",
f"建议缩小范围重试:只修复 {first_func}() 函数")
elif len(files) == 1 and ctx.gate_result.get("difficulty") == "hard":
self._emit(on_status, "suggest", "任务较复杂,建议拆分后分步执行")
def _do_reflection(self, ctx: TaskContext, on_status):
"""Ask LLM to analyze why the previous patch failed. One sentence, ≤50字."""
try:
error = ctx.verifier_output.get("error_detail", "") if ctx.verifier_output else ""
patches = ctx.generator_output.get("patches", []) if ctx.generator_output else []
modified_snippet = patches[0].get("modified", "")[:500] if patches else ""
reflection_prompt = (
f"你刚才生成的patch失败了。\n"
f"失败原因:{error[:300]}\n"
f"你修改的代码片段:\n{modified_snippet}\n\n"
f"分析这个patch为什么会失败根本原因是什么\n"
f"用一句话回答不超过50字。"
)
reflection = self.generator.llm.generate(
prompt=reflection_prompt,
system="你是代码审查专家,只做错误分析,不生成代码。",
max_tokens=100,
temperature=0.0,
)
ctx.reflection = reflection.strip()
logger.info("[orchestrator] reflection: %s", ctx.reflection)
self._emit(on_status, "reflection", f"反思:{ctx.reflection[:80]}")
except Exception as e:
logger.debug("Reflection failed (non-blocking): %s", e)
def _do_debug(self, ctx: TaskContext, on_status):
"""Debug Subagent: capture runtime info after test failure (non-blocking)."""
if not self.debug_subagent:
return
try:
self._emit(on_status, "debug", "调试子代理:采集运行时信息...")
debug_info = self.debug_subagent.investigate(ctx)
if debug_info:
ctx.debug_info = debug_info
self._emit(on_status, "debug_done", f"调试信息:{debug_info[:80]}")
else:
self._emit(on_status, "debug_done", "未获取到额外调试信息")
except Exception as e:
logger.debug("Debug subagent failed (non-blocking): %s", e)
def _do_review(self, ctx: TaskContext, on_status) -> dict:
"""Reviewer: 需求对齐审查(非阻塞)。成功后检查代码是否真正满足用户意图。"""
try:
from kaiwu.experts.reviewer import ReviewerExpert
reviewer = ReviewerExpert(llm=self.generator.llm)
self._emit(on_status, "review", "审查需求对齐...")
result = reviewer.review(ctx)
if result.get("aligned"):
self._emit(on_status, "review_done", "需求对齐确认")
else:
gap = result.get("gap", "")
self._emit(on_status, "review_gap", f"注意:{gap}")
return result
except Exception as e:
logger.debug("Reviewer failed (non-blocking): %s", e)
return {"aligned": True, "confidence": 0.0, "gap": ""}
def _emit(self, callback, stage: str, detail: str):
"""Emit status update if callback provided. Also logs to audit."""
if callback:
callback(stage, detail)
logger.info("[%s] %s", stage, detail)
self._audit.log(stage, detail)
def _get_max_retries(self, gate_result: dict) -> int:
"""Dynamic retry budget based on task difficulty and model strategy."""
difficulty = gate_result.get("difficulty", "easy")
base = self._RETRY_BY_DIFFICULTY.get(difficulty, self.MAX_RETRIES)
# 模型策略可以覆盖(小模型限制更严)
strategy_max = self._model_strategy.max_retries
return min(base, strategy_max)
@staticmethod
def _needs_realtime_data(user_input: str) -> bool:
"""检测用户输入是否需要实时数据(天气、股价、新闻等)。"""
keywords = [
"天气", "气温", "温度", "weather", "forecast",
"股价", "股票", "汇率", "价格", "price",
"新闻", "最新", "最近", "今天", "今日", "本周", "这周", "一周",
"news", "latest", "today", "recent",
]
lower = user_input.lower()
return any(kw in lower for kw in keywords)
def _record_value(self, project_root, gate_result, success, elapsed, ctx):
"""P2: Record task to local SQLite for value dashboard (non-blocking)."""
try:
self._value_tracker.record(
project_root=project_root,
expert_type=gate_result.get("expert_type", ""),
expert_name=gate_result.get("expert_name", "") or "",
success=success,
elapsed_s=elapsed,
retry_count=ctx.retry_count,
model=getattr(self, '_model_name', 'unknown'),
)
except Exception as e:
logger.debug("Value tracking failed (non-blocking): %s", e)
def _check_milestone(self, on_status):
"""P2: Check if total task count hits a milestone (50/100/200/500)."""
MILESTONES = {50, 100, 200, 500}
try:
total = self._value_tracker.get_total_task_count()
if total in MILESTONES:
expert_count = len(self.registry.list_experts(expert_type="generated")) if self.registry else 0
self._notifier.queue_milestone(total, expert_count, 0.0)
except Exception as e:
logger.debug("Milestone check failed (non-blocking): %s", e)
def _persist_reflection(self, project_root, ctx, gate_result, success):
"""Reflexion持久化任务完成后写入REFLECTION.md非阻塞"""
try:
if not ctx.reflection:
return
from kaiwu.memory.pattern_md import save_reflection
save_reflection(
project_root=project_root,
expert_type=gate_result.get("expert_type", "unknown"),
task_summary=ctx.user_input[:30],
reflection=ctx.reflection,
success=success,
)
except Exception as e:
logger.debug("Reflection persistence failed (non-blocking): %s", e)
def _record_flywheel(self, ctx: TaskContext, gate_result: dict, success: bool):
"""记录策略统计 + 用户错误模式 + 匿名遥测(全部非阻塞)。"""
errors = getattr(ctx, '_errors_encountered', [])
error_type = errors[-1] if errors else "unknown"
try:
sequence = EXPERT_SEQUENCES.get(
gate_result.get("expert_type", ""), ["generator", "verifier"]
)
self._strategy_stats.record(
error_type=error_type, sequence=sequence,
success=success, retries_used=ctx.retry_count,
)
except Exception as e:
logger.debug("Strategy stats failed (non-blocking): %s", e)
try:
self._user_patterns.record_task(errors, success)
except Exception as e:
logger.debug("User patterns failed (non-blocking): %s", e)
try:
self._telemetry.report(
error_type=error_type, retry_count=ctx.retry_count,
success=success, model=getattr(self, '_model_name', 'unknown'),
)
except Exception as e:
logger.debug("Telemetry failed (non-blocking): %s", e)
def _build_retry_hint(self, ctx: TaskContext, error_type: str) -> str:
"""按错误类型生成重试提示,注入 Generator prompt。携带上次生成的代码。"""
strategy = RETRY_STRATEGIES.get(error_type, RETRY_STRATEGIES["unknown"])
template = strategy.get("hint", "")
if not template:
hint = ""
else:
v = ctx.verifier_output or {}
try:
hint = template.format(
error_file=v.get("error_file", ""),
error_line=v.get("error_line", 0),
error_message=v.get("error_message", ""),
)
except (KeyError, ValueError):
hint = template
# 携带上次生成的代码让LLM看到自己的错误
last_code = ""
if ctx.generator_output:
patches = ctx.generator_output.get("patches", [])
if patches:
last_code = patches[0].get("modified", "") or patches[0].get("content", "")
last_code = last_code[:300]
if last_code:
hint += f"\n\n上次生成的代码(有问题):\n{last_code}\n\n请不要重复同样的错误。"
# 携带具体失败的测试名让LLM精确定位
failed_tests = (ctx.verifier_output or {}).get("failed_tests", [])
if failed_tests:
hint += "\n\n仍然失败的测试:\n" + "\n".join(f" - {t}" for t in failed_tests[:5])
# structured_failures精确传递用诊断句替代简单的期望/实际拼接
try:
from kaiwu.core.test_parser import parse_test_failures, generate_diagnosis
error_detail = (ctx.verifier_output or {}).get("error_detail", "")
if error_detail:
failures = parse_test_failures(error_detail)
if failures:
diagnosis = generate_diagnosis(failures)
if diagnosis:
hint += f"\n\n## 还在失败的具体测试(精确诊断)\n{diagnosis}\n"
else:
hint += "\n\n还在失败的具体测试:\n"
for f in failures[:4]:
hint += (
f"- {f['test_name']}: "
f"期望{f.get('expected', '?')}"
f"实际{f.get('actual', '?')}\n"
)
except Exception:
pass # 非阻塞
# TraceCoder: 携带历史教训摘要20+20=40效果
if ctx.attempt_history:
lessons = []
for rec in ctx.attempt_history[-3:]: # 最近3次
passed = rec.get("passed_tests", [])
failed = rec.get("failed_tests", [])
err = rec.get("error_message", "")
if passed or failed or err:
lessons.append(
f"{rec['attempt']}次:通过{len(passed)}个/失败{len(failed)}"
f"{' | 错误:' + err[:80] if err else ''}"
)
if lessons:
hint += "\n\n## 历史尝试记录(避免重复错误)\n" + "\n".join(lessons)
# Delta反馈告诉LLM相比上一次的进退情况
current_passed = (ctx.verifier_output or {}).get("tests_passed", 0)
if current_passed > 0:
last_passed = getattr(ctx, '_last_ver_passed', 0)
pre_passed = getattr(ctx, '_pre_test_passed', 0)
if last_passed > 0:
delta = current_passed - last_passed
if delta > 0:
hint += f"\n\n相比上一次尝试,新增通过了{delta}个测试,保持这个方向继续修复。"
elif delta < 0:
hint += f"\n\n注意:相比上一次退步了{-delta}个测试,请更保守地修改,不要破坏已通过的测试。"
elif delta == 0 and last_passed > pre_passed:
hint += f"\n\n上次修改有效(比初始多通过{last_passed - pre_passed}个),但还有测试失败,继续修复剩余问题。"
elif current_passed > pre_passed:
hint += f"\n\n已经比初始状态多通过了{current_passed - pre_passed}个测试,保持进展继续修复。"
ctx._last_ver_passed = current_passed
# 高通过率聚焦: 差1-2个测试时prepend精准指令
v = ctx.verifier_output or {}
tp = v.get("tests_passed", 0)
tt = v.get("tests_total", 0)
if tt > 0 and tp >= tt - 2 and tp > 0:
hint = (
f"你已通过 {tp}/{tt} 个测试,只有 {tt - tp} 个失败。\n"
f"只修复下面指出的问题,不要改动其他代码。\n\n"
) + hint
return hint
def _should_search(self, error_type: str, retry_count: int) -> bool:
"""按失败类型决定是否搜网络,不是统一在 retry>=2 时搜。"""
strategy = RETRY_STRATEGIES.get(error_type, RETRY_STRATEGIES["unknown"])
# import 错误:立刻搜
if strategy.get("search") and retry_count >= 1:
return True
# runtime 错误debug 一次后仍失败才搜
if error_type == "runtime" and retry_count >= 2:
return True
# assertion 连续 2 次同样错误:搜最优解法
if error_type == "assertion" and retry_count >= 2:
return True
# 通用 fallback第3次失败搜
if retry_count >= 3:
return True
return False
def _try_import_fix(self, ctx: TaskContext, on_status) -> bool:
"""尝试用 import_fixer 确定性修复缺失 import不调 LLM"""
try:
from kaiwu.tools.import_fixer import fix_missing_import
v = ctx.verifier_output or {}
error_msg = v.get("error_message", "")
error_file = v.get("error_file", "")
if not error_file or not error_msg:
return False
content = self.tools.read_file(error_file)
if content.startswith("[ERROR]"):
return False
fixed = fix_missing_import(content, error_msg)
if fixed and fixed != content:
self.tools.write_file(error_file, fixed)
self._emit(on_status, "import_fix", f"自动修复 import: {error_file}")
self.bus.emit("file_written", {"path": error_file})
return True
return False
except Exception as e:
logger.debug("Import fixer failed (non-blocking): %s", e)
return False
def _select_moe_expert(self, ctx: TaskContext, expert_type: str):
"""
MoE专家选择已移除独立Expert类。
依赖安装由EnvProber在Phase0处理存根实现由Generator通过scope处理。
始终返回None走默认pipeline。
"""
return None
def _recompute_gap(self, ctx: TaskContext, project_root: str) -> Gap:
"""重新运行测试并计算当前gap用于环境变化后更新状态。"""
try:
test_result = self.verifier.run_tests_only(ctx)
return self._gap_detector.compute(
test_result.get("output", ""),
project_root
)
except Exception:
return ctx.gap or Gap(GapType.UNKNOWN, 0.3, [], [], "", "")
def _build_trajectory_attempt(self, ctx: TaskContext, error_type: str) -> dict:
"""构建单次attempt的详细诊断记录供trajectory.json使用
所有字段try-except保护单字段失败不影响其他字段。
字符串字段截断到合理长度≤500字
"""
attempt = {"attempt": ctx.retry_count - 1} # retry_count已+1这里记录的是刚完成的attempt
# LLM输入输出从audit logger的llm_calls取最近一条
try:
llm_calls = self._audit._llm_calls
if llm_calls:
last_call = llm_calls[-1]
attempt["llm_prompt_tail"] = (last_call.get("prompt_preview", "") or "")[-400:]
attempt["llm_raw_output"] = (last_call.get("raw_output", "") or "")[:400]
attempt["llm_caller"] = last_call.get("caller", "")
else:
attempt["llm_prompt_tail"] = ""
attempt["llm_raw_output"] = ""
attempt["llm_caller"] = ""
except Exception:
attempt["llm_prompt_tail"] = ""
attempt["llm_raw_output"] = ""
attempt["llm_caller"] = ""
# patch生成结果
try:
patches = (ctx.generator_output or {}).get("patches", [])
attempt["patches_count"] = len(patches)
except Exception:
attempt["patches_count"] = 0
# patch apply结果
try:
v = ctx.verifier_output or {}
if v.get("error_type") == "patch_apply":
attempt["patch_apply_ok"] = False
attempt["patch_apply_error"] = (v.get("error_message", "") or v.get("error_detail", ""))[:300]
elif v:
# verifier跑到了测试阶段说明apply成功了
attempt["patch_apply_ok"] = True
attempt["patch_apply_error"] = ""
else:
attempt["patch_apply_ok"] = False
attempt["patch_apply_error"] = "no verifier output"
except Exception:
attempt["patch_apply_ok"] = False
attempt["patch_apply_error"] = ""
# 修改行数
try:
patches = (ctx.generator_output or {}).get("patches", [])
modified_lines = 0
for p in patches:
orig = p.get("original", "") or ""
mod = p.get("modified", "") or p.get("content", "") or ""
modified_lines += abs(len(mod.splitlines()) - len(orig.splitlines()))
attempt["modified_lines"] = modified_lines
except Exception:
attempt["modified_lines"] = 0
# verifier测试结果
try:
v = ctx.verifier_output or {}
attempt["tests_passed"] = v.get("tests_passed", 0)
attempt["tests_total"] = v.get("tests_total", 0)
attempt["test_output_tail"] = (v.get("error_detail", "") or "")[-300:]
except Exception:
attempt["tests_passed"] = 0
attempt["tests_total"] = 0
attempt["test_output_tail"] = ""
# 错误类型
try:
attempt["error_type"] = error_type or ""
except Exception:
attempt["error_type"] = ""
return attempt
def _regression_guard(self, ctx: TaskContext, ver_result: dict, on_status):
"""不退步保护:如果测试通过数退步,回滚到最优状态;否则更新快照。
非阻塞,失败静默。
"""
if not ver_result:
return
try:
new_passed = ver_result.get("tests_passed", 0)
# 获取本次修改的文件列表
modified_files = []
if ctx.generator_output:
for p in ctx.generator_output.get("patches", []):
f = p.get("file", "")
if f:
modified_files.append(f)
if new_passed < ctx.best_tests_passed and ctx.best_code_snapshot:
# 退步了,回滚到最优状态
for fname, content in ctx.best_code_snapshot.items():
try:
self.tools.write_file(fname, content)
except Exception:
pass
failing_tests = ver_result.get("failed_tests", [])
self._emit(on_status, "regression_guard",
f"新patch退步{new_passed}<{ctx.best_tests_passed}),已回滚到最优状态")
ctx.retry_hint = (
f"上次修改后只有{new_passed}个测试通过,"
f"比之前的{ctx.best_tests_passed}个更少。\n"
f"还在失败的测试:{failing_tests[:3]}"
)
else:
# 进步了或持平,更新最优状态
ctx.best_tests_passed = new_passed
if modified_files:
snapshot = {}
for f in modified_files:
try:
content = self.tools.read_file(f)
if not content.startswith("[ERROR]"):
snapshot[f] = content
except Exception:
pass
if snapshot:
ctx.best_code_snapshot = snapshot
except Exception as e:
logger.debug("Regression guard failed (non-blocking): %s", e)