Files
kwcode/kaiwu/core/orchestrator.py
Val-sss ce8646f57e feat: P0+P1 optimization — circuit breaker, gate confidence, experience replay, session state
- P0-1: Verifier structured error output (error_type/file/line/message extraction)
- P0-2: Circuit breaker + scope narrowing (syntax/import fast-break, 3x same-error hard-break, auto-narrow on 2nd failure)
- P0-3: Gate confidence estimation (keyword signal scoring, low-confidence retry reduction)
- P1-1: Experience Replay via BM25 trajectory similarity search
- P1-2: SessionState multi-turn coherence + attention decay countermeasure
- P1-3: Locator minimal context extraction (function boundary detection, comment stripping, 60-line cap)
- Add CONTRIBUTING.md with architecture red lines and PR standards
- Update README contributing section with quick reference

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

632 lines
30 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
from typing import Optional
from kaiwu.core.context import TaskContext
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.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
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.stats.value_tracker import ValueTracker
from kaiwu.notification.flywheel_notifier import FlywheelNotifier
logger = logging.getLogger(__name__)
# RED-2: Fixed sequences per expert_type. No LLM decides next step.
EXPERT_SEQUENCES = {
"locator_repair": ["locator", "generator", "verifier"],
"codegen": ["generator", "verifier"],
"refactor": ["locator", "generator", "verifier"],
"doc": ["locator", "generator"],
"office": ["office"],
"chat": ["chat"],
"vision": ["vision"],
}
class PipelineOrchestrator:
"""Deterministic expert pipeline orchestrator."""
MAX_RETRIES = 3 # Default, overridden by _get_max_retries()
_RETRY_BY_DIFFICULTY = {"easy": 2, "hard": 4} # Dynamic budget
def __init__(
self,
locator: LocatorExpert,
generator: GeneratorExpert,
verifier: VerifierExpert,
search_augmentor: SearchAugmentorExpert,
office_handler: OfficeHandlerExpert,
tool_executor: ToolExecutor,
memory: KaiwuMemory,
registry: ExpertRegistry | None = None,
trajectory_collector: TrajectoryCollector | None = None,
ab_tester: ABTester | None = None,
chat_expert: ChatExpert | None = None,
debug_subagent=None,
vision_expert=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()
def run(
self,
user_input: str,
gate_result: dict,
project_root: str,
on_status=None,
no_search: bool = False,
skip_checkpoint: bool = False,
pre_search_results: str = "",
image_paths: list = 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()
# Store project_root for Gate 2 backtest use
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", ""),
)
# 处理图片路径
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")
# ── Pre-search results injection (P1-B) ──
if pre_search_results:
ctx.search_results = pre_search_results
ctx.search_triggered = True
self._emit(on_status, "search", "已预加载实时数据")
# ── KWCODE.md rules injection ──
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
# Prepend to expert_system_prompt so it flows to all experts
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类型直接回复不走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
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,
}
# Gate 3: AB test — check if a candidate expert should be used for this task
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
# Override gate_result to use the candidate expert's pipeline
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:
# Check if any candidate is in AB testing for this type (baseline run)
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
# Use custom pipeline from expert registry if available, else default
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', '?')}")
# ── 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):
self._emit(on_status, "search", "检测到实时数据需求,预搜索...")
ctx.search_results = self.search_augmentor.search(ctx)
ctx.search_triggered = True
if ctx.search_results:
self._emit(on_status, "search_done", f"搜索完成,注入{len(ctx.search_results)}字参考信息")
# ── Checkpoint: snapshot before execution (skip in multi-task to avoid race) ──
checkpoint = Checkpoint(project_root)
checkpoint_saved = False
if not skip_checkpoint:
checkpoint_saved = checkpoint.save()
if not checkpoint_saved:
self._emit(on_status, "warning", "无法创建文件快照,任务失败时需手动还原")
# Dynamic retry budget based on task difficulty
max_retries = self._get_max_retries(gate_result)
# Low confidence: reduce retry budget (not worth many attempts)
confidence = gate_result.get("confidence", 1.0)
if confidence < 0.6 and expert_type not in ("chat", "office", "vision"):
max_retries = min(max_retries, 2)
self._emit(on_status, "low_confidence",
f"任务分类置信度较低({confidence:.0%}),减少重试次数")
while ctx.retry_count < max_retries:
success = self._run_sequence(sequence, ctx, on_status)
# Notify locator of task result (graph stats + incremental update)
self._notify_locator(ctx, success)
if success:
elapsed = time.time() - start_time
checkpoint.discard() # Clean up snapshot on success
# Reviewer: 需求对齐审查(非阻塞,不影响成功判定)
review_result = self._do_review(ctx, on_status)
# Save to memory on success (with elapsed for expert/pattern tracking)
self.memory.save(project_root, ctx, elapsed=elapsed)
# Update expert registry stats
expert_name = gate_result.get("expert_name")
if expert_name and self.registry:
self.registry.update_stats(expert_name, success=True, latency=elapsed)
# Flywheel: record trajectory and detect patterns (non-blocking)
self._record_trajectory(ctx, True, elapsed, on_status)
# Gate 3: record AB test result if this task is part of an AB test
self._record_ab_result(ab_candidate_name, ab_used_new, True, elapsed, on_status)
# P2: Value tracking (local SQLite)
self._record_value(project_root, gate_result, True, elapsed, ctx)
# P2: Milestone check
self._check_milestone(on_status)
# Reflexion持久化成功时也记录注意事项
self._persist_reflection(project_root, ctx, gate_result, success=True)
return {
"success": True,
"context": ctx,
"error": None,
"elapsed": elapsed,
}
ctx.retry_count += 1
error_detail = ""
if ctx.verifier_output:
error_detail = ctx.verifier_output.get("error_detail", "")
# Save failure info for retry strategy
ctx.previous_failure = error_detail
# ── Circuit breaker: same error_type streak ──
current_error_type = ""
if ctx.verifier_output:
current_error_type = ctx.verifier_output.get("error_type", "unknown")
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}
# Fast circuit break: syntax errors don't improve with retries
if current_error_type == "syntax" and ctx.retry_count >= 1:
self._emit(on_status, "circuit_break", "语法错误重试无效,模型能力不足以完成此任务")
break
# Fast circuit break: missing imports need user action
if current_error_type == "import":
missing = ctx.verifier_output.get("error_message", "") if ctx.verifier_output else ""
self._emit(on_status, "circuit_break", f"缺少依赖:{missing},请先安装")
break
# Hard circuit break: same error type 3 times in a row
if ctx._error_type_streak["count"] >= 3:
self._emit(on_status, "circuit_break",
f"同类错误({current_error_type})连续{ctx._error_type_streak['count']}次,停止重试")
break
# ── Scope narrowing: on 2nd failure, reduce to first file+function ──
if ctx.retry_count == 2 and ctx.locator_output:
files = ctx.locator_output.get("relevant_files", [])
funcs = ctx.locator_output.get("relevant_functions", [])
if len(files) > 1 and funcs:
ctx.locator_output = {
"relevant_files": [files[0]],
"relevant_functions": [funcs[0]],
"edit_locations": ctx.locator_output.get("edit_locations", [])[:1],
"method": "scope_narrowed",
}
if ctx.relevant_code_snippets:
ctx.relevant_code_snippets = {
files[0]: ctx.relevant_code_snippets.get(files[0], "")
}
self._emit(on_status, "scope_narrow", f"缩小范围:只修 {funcs[0]}()")
self._emit(on_status, "retry", f"{ctx.retry_count}次尝试失败:{error_detail[:100]}")
# Reflection before 2nd retry: ask LLM why the patch failed
if ctx.retry_count == 1 and ctx.verifier_output and ctx.generator_output:
self._do_reflection(ctx, on_status)
# Debug Subagent: capture runtime info on failure (test failures only)
if ctx.retry_count >= 1 and ctx.verifier_output:
self._do_debug(ctx, on_status)
# Set retry strategy: each retry uses a different approach
ctx.retry_strategy = ctx.retry_count # 0→1→2
# Trigger SearchAugmentor: failed 2x OR hard task failed 1x
should_search = (
ctx.retry_count >= 2
or (gate_result.get("difficulty") == "hard" and ctx.retry_count >= 1)
)
if should_search and not ctx.search_triggered and not no_search:
self._emit(on_status, "search", "触发搜索增强...")
ctx.search_results = self.search_augmentor.search(ctx)
ctx.search_triggered = True
self._emit(on_status, "search_done", f"搜索完成,注入{len(ctx.search_results)}字参考信息")
# Reset expert outputs for retry (RED-3: fresh context each attempt)
ctx.locator_output = None
ctx.generator_output = None
ctx.verifier_output = None
ctx.relevant_code_snippets = {}
# Clear ephemeral debug info to prevent context pollution
ctx.debug_info = ""
elapsed = time.time() - start_time
# ── Checkpoint: restore on failure ──
if checkpoint_saved:
restored = checkpoint.restore()
if restored:
self._emit(on_status, "checkpoint", "已还原到任务执行前的状态")
else:
self._emit(on_status, "warning", "还原失败,请手动检查文件")
# Downgrade suggestion
self._suggest_downgrade(ctx, on_status)
# Record failure in pattern memory
self.memory.save_failure(project_root, ctx, elapsed=elapsed)
# Update expert registry stats on failure
expert_name = gate_result.get("expert_name")
if expert_name and self.registry:
self.registry.update_stats(expert_name, success=False, latency=elapsed)
# Flywheel: record failure trajectory
self._record_trajectory(ctx, False, elapsed, on_status)
# Gate 3: record AB test failure if this task is part of an AB test
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)
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", "定位中...")
result = self.locator.run(ctx)
if not result:
self._emit(on_status, "locator_fail", "定位失败")
return False
files = result.get("relevant_files", [])
funcs = result.get("relevant_functions", [])
self._emit(on_status, "locator_done", f"文件:{', '.join(files[:3])} | 函数:{', '.join(funcs[:3])}")
elif step == "generator":
self._emit(on_status, "generator", "生成patch...")
result = self.generator.run(ctx)
if not result:
self._emit(on_status, "generator_fail", "生成失败")
return False
n_patches = len(result.get("patches", []))
self._emit(on_status, "generator_done", f"生成{n_patches}个patch")
elif step == "verifier":
self._emit(on_status, "verifier", "验证中...")
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]}")
return False
tp = result.get("tests_passed", 0)
tt = result.get("tests_total", 0)
self._emit(on_status, "verifier_done", f"语法OK | 测试:{tp}/{tt}")
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 _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)
# On success, check for flywheel candidates
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)")
# Auto-graduation is handled inside record_ab_result when total >= 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": ""}
@staticmethod
def _emit(callback, stage: str, detail: str):
"""Emit status update if callback provided."""
if callback:
callback(stage, detail)
logger.info("[%s] %s", stage, detail)
def _get_max_retries(self, gate_result: dict) -> int:
"""Dynamic retry budget based on task difficulty."""
difficulty = gate_result.get("difficulty", "easy")
return self._RETRY_BY_DIFFICULTY.get(difficulty, self.MAX_RETRIES)
@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)