feat: Planner 오케스트레이션 루프 구현

이중 루프 구조:
- 내부 루프 (Planner 자가검증, 최대 3회):
  계획 → 코딩 → Planner가 결과 검증 → 미달 시 추가 태스크 → 반복
  Planner가 만족할 때까지 자체적으로 보완 작업 진행
- 외부 루프 (Reviewer, 최대 2회):
  Planner 만족 → Reviewer 검토 → 반려 시 피드백으로 재계획

새 메서드:
- planner_verify(): Planner가 계획 달성도 자가 검증
- _read_recent_files(): 프로젝트 파일 읽기 공용 헬퍼
This commit is contained in:
2026-03-06 23:14:05 +09:00
parent 47a8c41a81
commit 52402065f3
2 changed files with 151 additions and 62 deletions

View File

@@ -352,74 +352,105 @@ async def _handle_task(message: discord.Message, text: str, ws):
)
return
# 2. Code + Review (실패 시 재계획 루프)
# 2. Planner 오케스트레이션 루프 (외부: 리뷰어, 내부: 자가검증)
MAX_PLANNER_LOOPS = 3 # Planner 내부 자가검증 반복 제한
review = None
code_outputs = []
for attempt in range(1 + MAX_REVIEW_RETRIES):
attempt_label = f" (재시도 {attempt})" if attempt > 0 else ""
all_code_outputs = []
# 재시도 시: 리뷰 피드백으로 재계획
if attempt > 0:
for review_attempt in range(1 + MAX_REVIEW_RETRIES):
review_label = f" (리뷰 재시도 {review_attempt})" if review_attempt > 0 else ""
# 리뷰어 반려 시: 피드백으로 재계획
if review_attempt > 0:
feedback = review.get("summary", str(review))
await message.channel.send(
embed=discord.Embed(
title=f"🔄 재계획 {attempt}/{MAX_REVIEW_RETRIES}",
description="리뷰 피드백을 바탕으로 다시 계획합니다.",
color=0xF39C12,
title=f"🔄 리뷰어 피드백 반영 재계획",
description=feedback[:500],
color=0xE74C3C,
)
)
replan_request = (
f"## 원래 요청\n{text}\n\n"
f"## 이전 리뷰 피드백 (반드시 반영)\n{feedback}\n\n"
f"이전 시도가 실패했습니다. 피드백을 분석하고 태스크를 재설계하세요."
f"## 리뷰 피드백 (반드시 반영)\n{feedback}\n\n"
f"피드백을 분석하고 태스크를 재설계하세요."
)
plan = await pipeline.plan(replan_request)
tasks = plan.get("tasks", [])
if not tasks:
break
if tasks:
task_list = "\n".join(
f" {t.get('title', '?')}" for t in tasks[:10]
)
# ── Planner 내부 루프: 계획 → 코딩 → 자가검증 → 추가작업 ──
for planner_round in range(MAX_PLANNER_LOOPS):
round_label = f" (보완 {planner_round})" if planner_round > 0 else ""
# 코딩
code_embed = discord.Embed(
title=f"⚙️ 코딩 중...{review_label}{round_label} ({len(tasks)}개)",
description="\n".join(
f"{t.get('title', '?')[:60]}" for t in tasks[:10]
),
color=0xE67E22,
)
code_msg = await message.channel.send(embed=code_embed)
code_outputs = await pipeline.code_parallel(tasks)
all_code_outputs = code_outputs # 최신 결과 유지
error_count = sum(1 for o in code_outputs if o.startswith("[ERROR]"))
code_embed.title = f"✅ 코딩 완료{round_label} ({len(tasks) - error_count}/{len(tasks)})"
code_embed.color = 0x2ECC71 if error_count == 0 else 0xF39C12
await code_msg.edit(embed=code_embed)
# Planner 자가검증
verify_embed = discord.Embed(
title="🔍 Planner 자가 검증 중...",
color=0xF39C12,
)
verify_msg = await message.channel.send(embed=verify_embed)
verification = await pipeline.planner_verify(text, plan, code_outputs)
satisfied = verification.get("satisfied", True)
if isinstance(satisfied, str):
satisfied = satisfied.lower() in ("true", "yes")
verify_embed.title = f"{'' if satisfied else '🔄'} Planner 검증{round_label}"
verify_embed.description = verification.get("feedback", "")[:500]
verify_embed.color = 0x2ECC71 if satisfied else 0xF39C12
await verify_msg.edit(embed=verify_embed)
if satisfied:
break # Planner 만족 → 리뷰어에게 전달
# 추가 태스크가 있으면 계속
additional = verification.get("additional_tasks", [])
if additional:
tasks = additional
task_list = "\n".join(f"{t.get('title', '?')}" for t in tasks[:10])
await message.channel.send(
embed=discord.Embed(
title=f"📝 재계획 결과",
description=f"태스크 {len(tasks)}",
color=0x2ECC71,
title=f"📝 추가 작업 {len(additional)}",
color=0xF39C12,
).add_field(name="태스크", value=task_list[:1000], inline=False)
)
else:
break # 추가 태스크 없으면 종료
# 코딩 진행 표시
code_embed = discord.Embed(
title=f"⚙️ 코딩 중...{attempt_label} ({len(tasks)}개 에이전트)",
description="\n".join(
f"{t.get('title', '?')[:60]}" for t in tasks[:10]
),
color=0xE67E22,
)
code_msg = await message.channel.send(embed=code_embed)
code_outputs = await pipeline.code_parallel(tasks)
error_count = sum(1 for o in code_outputs if o.startswith("[ERROR]"))
code_embed.title = f"✅ 코딩 완료{attempt_label} ({len(tasks) - error_count}/{len(tasks)} 성공)"
code_embed.color = 0x2ECC71 if error_count == 0 else 0xF39C12
await code_msg.edit(embed=code_embed)
# 리뷰
# ── 외부 리뷰어 ──
review_embed = discord.Embed(
title="🔍 리뷰 중...",
title="🔍 리뷰어 검토 중...",
color=0xF39C12,
)
review_msg = await message.channel.send(embed=review_embed)
review = await pipeline.batch_review(tasks, code_outputs)
review = await pipeline.batch_review(tasks, all_code_outputs)
passed = review.get("passed", True)
if isinstance(passed, str):
passed = passed.lower() in ("true", "yes", "pass")
review_embed.title = f"{'' if passed else '⚠️'} 리뷰 결과{attempt_label}"
review_embed.title = f"{'' if passed else '⚠️'} 리뷰 결과{review_label}"
review_embed.description = review.get("summary", str(review))[:500]
review_embed.color = 0x2ECC71 if passed else 0xE74C3C
await review_msg.edit(embed=review_embed)
@@ -428,7 +459,7 @@ async def _handle_task(message: discord.Message, text: str, ws):
break
# 3. 총평
summary = await pipeline.summarize(text, plan, code_outputs, review)
summary = await pipeline.summarize(text, plan, all_code_outputs, review)
summary_embed = discord.Embed(
title=f"📊 {summary.get('title', '작업 완료')}",