Files
AX-Copilot-Codex/src/AxCopilot/Services/DraftQueueProcessorService.cs
lacvet 33c1db4dae
Some checks failed
Release Gate / gate (push) Has been cancelled
에이전트 선택적 탐색 구조 개선과 경고 정리 반영
- claude-code 선택적 탐색 흐름을 참고해 Cowork/Code 시스템 프롬프트에서 folder_map 상시 선행 지시를 완화하고 glob/grep 기반 좁은 탐색을 우선하도록 조정함

- FolderMapTool 기본 depth를 2로, include_files 기본값을 false로 낮추고 MultiReadTool 최대 파일 수를 8개로 줄여 초기 과탐색 폭을 보수적으로 조정함

- AgentLoopExplorationPolicy partial을 추가해 탐색 범위 분류, broad-scan corrective hint, exploration_breadth 성능 로그를 연결함

- AgentLoopService에 탐색 범위 가이드 주입과 실행 중 탐색 폭 추적을 추가하고, 좁은 질문에서 반복적인 folder_map/대량 multi_read를 교정하도록 정리함

- DocxToHtmlConverter nullable 경고를 수정해 Release 빌드 경고 0 / 오류 0 기준을 다시 충족함

- README와 docs/DEVELOPMENT.md에 2026-04-09 10:36 (KST) 기준 개발 이력을 반영함
2026-04-09 14:27:59 +09:00

147 lines
5.7 KiB
C#

using AxCopilot.Models;
namespace AxCopilot.Services;
/// <summary>
/// 드래프트 큐 항목의 실행 전이와 재시도 정책을 담당합니다.
/// ChatWindow가 직접 상태 전이를 조합하지 않도록 별도 서비스로 분리합니다.
/// </summary>
public sealed class DraftQueueProcessorService
{
public bool CanStartNext(ChatSessionStateService? session, string tab)
=> session?.GetNextQueuedDraft(tab) != null;
public DraftQueueItem? TryStartNext(ChatSessionStateService? session, string tab, ChatStorageService? storage = null, string? preferredDraftId = null, TaskRunService? taskRuns = null)
{
if (session == null)
return null;
DraftQueueItem? next = null;
if (!string.IsNullOrWhiteSpace(preferredDraftId))
{
next = session.GetDraftQueueItems(tab)
.FirstOrDefault(x => string.Equals(x.Id, preferredDraftId, StringComparison.OrdinalIgnoreCase));
if (next != null &&
!string.Equals(next.State, "queued", StringComparison.OrdinalIgnoreCase))
{
session.ResetDraftToQueued(tab, next.Id, storage);
next = session.GetDraftQueueItems(tab)
.FirstOrDefault(x => string.Equals(x.Id, preferredDraftId, StringComparison.OrdinalIgnoreCase));
}
}
next ??= session.GetNextQueuedDraft(tab);
if (next == null)
return null;
if (!session.MarkDraftRunning(tab, next.Id, storage))
return null;
taskRuns?.StartQueueRun(tab, next.Id, next.Text);
return session.GetDraftQueueItems(tab)
.FirstOrDefault(x => string.Equals(x.Id, next.Id, StringComparison.OrdinalIgnoreCase))
?? next;
}
public bool Complete(ChatSessionStateService? session, string tab, string draftId, ChatStorageService? storage = null, TaskRunService? taskRuns = null)
{
var completed = session?.MarkDraftCompleted(tab, draftId, storage) ?? false;
if (completed)
taskRuns?.CompleteQueueRun(tab, draftId, "대기열 작업 완료", "completed");
return completed;
}
public bool HandleFailure(ChatSessionStateService? session, string tab, string draftId, string? error, bool cancelled = false, int maxAutoRetries = 3, ChatStorageService? storage = null, TaskRunService? taskRuns = null)
{
if (session == null)
return false;
if (cancelled)
{
var reset = session.ResetDraftToQueued(tab, draftId, storage);
if (reset)
taskRuns?.CompleteQueueRun(tab, draftId, string.IsNullOrWhiteSpace(error) ? "대기열 작업 중단" : error, "cancelled");
return reset;
}
var handled = session.ScheduleDraftRetry(tab, draftId, error, maxAutoRetries, storage);
if (handled)
{
var item = session.GetDraftQueueItems(tab)
.FirstOrDefault(x => string.Equals(x.Id, draftId, StringComparison.OrdinalIgnoreCase));
var blocked = item?.NextRetryAt.HasValue == true;
taskRuns?.CompleteQueueRun(
tab,
draftId,
string.IsNullOrWhiteSpace(error) ? (blocked ? "재시도 대기" : "대기열 작업 실패") : error,
blocked ? "blocked" : "failed");
}
return handled;
}
public int PromoteReadyBlockedItems(ChatSessionStateService? session, string tab, ChatStorageService? storage = null)
{
if (session == null)
return 0;
var promoted = 0;
foreach (var item in session.GetDraftQueueItems(tab)
.Where(x => string.Equals(x.State, "queued", StringComparison.OrdinalIgnoreCase)
&& x.NextRetryAt.HasValue
&& x.NextRetryAt.Value <= DateTime.Now)
.ToList())
{
if (session.ResetDraftToQueued(tab, item.Id, storage))
promoted++;
}
return promoted;
}
public int ClearCompleted(ChatSessionStateService? session, string tab, ChatStorageService? storage = null)
=> ClearByState(session, tab, "completed", storage);
public int ClearFailed(ChatSessionStateService? session, string tab, ChatStorageService? storage = null)
=> ClearByState(session, tab, "failed", storage);
/// <summary>대기 중인 항목을 모두 제거합니다 (중지 시 사용).</summary>
public int ClearQueued(ChatSessionStateService? session, string tab, ChatStorageService? storage = null)
=> ClearByState(session, tab, "queued", storage);
/// <summary>실행 중인 항목을 실패로 전환합니다 (중지 시 사용).</summary>
public int CancelRunning(ChatSessionStateService? session, string tab, ChatStorageService? storage = null)
{
if (session == null) return 0;
int count = 0;
foreach (var item in session.GetDraftQueueItems(tab)
.Where(x => string.Equals(x.State, "running", StringComparison.OrdinalIgnoreCase))
.ToList())
{
if (session.RemoveDraft(tab, item.Id, storage))
count++;
}
return count;
}
private static int ClearByState(ChatSessionStateService? session, string tab, string state, ChatStorageService? storage)
{
if (session == null)
return 0;
var removed = 0;
foreach (var item in session.GetDraftQueueItems(tab)
.Where(x => string.Equals(x.State, state, StringComparison.OrdinalIgnoreCase))
.ToList())
{
if (session.RemoveDraft(tab, item.Id, storage))
removed++;
}
return removed;
}
}