AX Agent 실행 마감 helper 통합과 계획 노출 최소화
Some checks failed
Release Gate / gate (push) Has been cancelled

- send와 regenerate가 같은 실행/예외/취소/최종 커밋/후처리 helper를 타도록 ExecutePreparedTurnAsync 추가

- 계획 이벤트는 기본적으로 얇은 요약 pill만 노출하고 debug에서만 큰 카드가 보이도록 조정

- README와 DEVELOPMENT 문서에 2026-04-05 18:08 (KST) 기준 이력 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
This commit is contained in:
2026-04-05 15:43:44 +09:00
parent e4fddca53c
commit 57be80af3c
3 changed files with 127 additions and 93 deletions

View File

@@ -8372,30 +8372,17 @@ public partial class ChatWindow : Window
}
}
var response = await _chatEngine.ExecutePreparedAsync(
var executionOutcome = await ExecutePreparedTurnAsync(
conv,
runTab,
originTab,
preparedExecution,
(messages, token) => RunAgentLoopAsync(runTab, originTab, conv, messages, token),
(messages, token) => _llm.SendAsync(messages.ToList(), token),
_streamCts.Token);
assistantContent = response;
StopAiIconPulse();
_cachedStreamContent = response;
draftSucceeded = true;
}
catch (OperationCanceledException)
{
var finalized = _chatEngine.FinalizeExecutionContent(assistantContent, cancelled: true);
assistantContent = finalized.Content;
draftCancelled = finalized.Cancelled;
draftFailure = finalized.FailureReason;
}
catch (Exception ex)
{
var finalized = _chatEngine.FinalizeExecutionContent(assistantContent, ex);
assistantContent = finalized.Content;
ShowToast("실패한 요청은 작업 요약에서 다시 시도할 수 있습니다.", "\uE783", 2600);
draftFailure = finalized.FailureReason;
"응답 생성 중...");
conv = executionOutcome.Conversation;
assistantContent = executionOutcome.AssistantContent;
draftSucceeded = executionOutcome.DraftSucceeded;
draftCancelled = executionOutcome.DraftCancelled;
draftFailure = executionOutcome.DraftFailure;
}
finally
{
@@ -8405,18 +8392,7 @@ public partial class ChatWindow : Window
_llm.ClearRouteOverride();
UpdateModelLabel();
}
ResetStreamingUiState();
}
lock (_convLock)
{
var session = ChatSession;
assistantContent = _chatEngine.FinalizeAssistantTurn(session, conv, runTab, assistantContent, _storage);
_currentConversation = session?.CurrentConversation ?? conv;
conv = _currentConversation!;
}
FinalizeConversationTurn(originTab, conv);
FinalizeQueuedDraft(originTab, queuedDraftId, draftSucceeded, draftCancelled, draftFailure);
}
@@ -8553,6 +8529,87 @@ public partial class ChatWindow : Window
_ = Dispatcher.BeginInvoke(new Action(() => StartNextQueuedDraftIfAny()), DispatcherPriority.Background);
}
private sealed record PreparedTurnOutcome(
ChatConversation Conversation,
string AssistantContent,
bool DraftSucceeded,
bool DraftCancelled,
string? DraftFailure);
private async Task<PreparedTurnOutcome> ExecutePreparedTurnAsync(
ChatConversation conversation,
string runTab,
string rememberTab,
AxAgentExecutionEngine.PreparedExecution preparedExecution,
string busyStatus)
{
_isStreaming = true;
_streamRunTab = runTab;
BtnSend.IsEnabled = false;
BtnSend.Visibility = Visibility.Collapsed;
BtnStop.Visibility = Visibility.Visible;
if (runTab == "Cowork" || runTab == "Code")
BtnPause.Visibility = Visibility.Visible;
_streamCts = new CancellationTokenSource();
ForceScrollToEnd();
var assistantContent = string.Empty;
var draftSucceeded = false;
var draftCancelled = false;
string? draftFailure = null;
_activeStreamText = null;
_cachedStreamContent = "";
_displayedLength = 0;
_cursorVisible = true;
_aiIconPulseStopped = false;
_streamStartTime = DateTime.UtcNow;
_elapsedTimer.Start();
SetStatus(busyStatus, spinning: true);
try
{
var response = await _chatEngine.ExecutePreparedAsync(
preparedExecution,
(messages, token) => RunAgentLoopAsync(runTab, rememberTab, conversation, messages, token),
(messages, token) => _llm.SendAsync(messages.ToList(), token),
_streamCts.Token);
assistantContent = response;
StopAiIconPulse();
_cachedStreamContent = response;
draftSucceeded = true;
}
catch (OperationCanceledException)
{
var finalized = _chatEngine.FinalizeExecutionContent(assistantContent, cancelled: true);
assistantContent = finalized.Content;
draftCancelled = finalized.Cancelled;
draftFailure = finalized.FailureReason;
}
catch (Exception ex)
{
var finalized = _chatEngine.FinalizeExecutionContent(assistantContent, ex);
assistantContent = finalized.Content;
draftFailure = finalized.FailureReason;
ShowToast("실패한 요청은 작업 요약에서 다시 시도할 수 있습니다.", "\uE783", 2600);
}
finally
{
ResetStreamingUiState();
}
lock (_convLock)
{
var session = ChatSession;
assistantContent = _chatEngine.FinalizeAssistantTurn(session, conversation, runTab, assistantContent, _storage);
_currentConversation = session?.CurrentConversation ?? conversation;
conversation = _currentConversation!;
}
FinalizeConversationTurn(rememberTab, conversation);
return new PreparedTurnOutcome(conversation, assistantContent, draftSucceeded, draftCancelled, draftFailure);
}
private void ScheduleExecutionHistoryRender(bool autoScroll = true)
{
_pendingExecutionHistoryAutoScroll |= autoScroll;
@@ -10287,10 +10344,29 @@ public partial class ChatWindow : Window
{
var logLevel = _settings.Settings.Llm.AgentLogLevel;
// Planning 이벤트는 단계 목록 카드로 별도 렌더링
// Planning 이벤트는 기본적으로 얇은 요약만 보이고, debug에서만 큰 카드로 펼칩니다.
if (evt.Type == AgentEventType.Planning && evt.Steps is { Count: > 0 })
{
AddPlanningCard(evt);
if (string.Equals(logLevel, "debug", StringComparison.OrdinalIgnoreCase))
{
AddPlanningCard(evt);
}
else
{
var compactPrimaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var compactSecondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var compactHintBg = TryFindResource("HintBackground") as Brush
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
var compactBorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
var compactAccentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
var summary = !string.IsNullOrWhiteSpace(evt.Summary)
? evt.Summary!
: $"계획 {evt.Steps.Count}단계";
var pill = CreateCompactEventPill(summary, compactPrimaryText, compactSecondaryText, compactHintBg, compactBorderBrush, compactAccentBrush);
pill.Opacity = 0;
pill.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(160)));
MessagePanel.Children.Add(pill);
}
return;
}
@@ -10876,64 +10952,15 @@ public partial class ChatWindow : Window
private async Task SendRegenerateAsync(ChatConversation conv)
{
var runTab = NormalizeTabName(conv.Tab);
_isStreaming = true;
_streamRunTab = runTab;
BtnSend.IsEnabled = false;
BtnSend.Visibility = Visibility.Collapsed;
BtnStop.Visibility = Visibility.Visible;
_streamCts = new CancellationTokenSource();
ForceScrollToEnd(); // 응답 시작 시 강제 하단 이동
var assistantContent = string.Empty;
_activeStreamText = null;
_cachedStreamContent = "";
_displayedLength = 0;
_cursorVisible = true;
_aiIconPulseStopped = false;
_streamStartTime = DateTime.UtcNow;
_elapsedTimer.Start();
SetStatus("에이전트 작업 중...", spinning: true);
try
{
List<ChatMessage> sendMessages;
AxAgentExecutionEngine.PreparedExecution preparedExecution;
lock (_convLock)
preparedExecution = PrepareExecutionForConversation(conv, runTab, null);
var executionMode = preparedExecution.Mode;
sendMessages = preparedExecution.Messages;
var response = await _chatEngine.ExecutePreparedAsync(
preparedExecution,
(messages, token) => RunAgentLoopAsync(runTab, runTab, conv, messages, token),
(messages, token) => _llm.SendAsync(messages.ToList(), token),
_streamCts.Token);
assistantContent = response;
StopAiIconPulse();
_cachedStreamContent = response;
}
catch (OperationCanceledException)
{
assistantContent = _chatEngine.FinalizeExecutionContent(assistantContent, cancelled: true).Content;
}
catch (Exception ex)
{
assistantContent = _chatEngine.FinalizeExecutionContent(assistantContent, ex).Content;
ShowToast("실패한 요청은 작업 요약에서 다시 시도할 수 있습니다.", "\uE783", 2600);
}
finally
{
ResetStreamingUiState();
}
AxAgentExecutionEngine.PreparedExecution preparedExecution;
lock (_convLock)
{
var session = ChatSession;
assistantContent = _chatEngine.FinalizeAssistantTurn(session, conv, runTab, assistantContent, _storage);
_currentConversation = session?.CurrentConversation ?? conv;
conv = _currentConversation!;
}
FinalizeConversationTurn(conv.Tab ?? _activeTab, conv);
preparedExecution = PrepareExecutionForConversation(conv, runTab, null);
await ExecutePreparedTurnAsync(
conv,
runTab,
conv.Tab ?? _activeTab,
preparedExecution,
"에이전트 작업 중...");
}
/// <summary>채팅 본문 폭을 세 탭에서 동일한 기준으로 맞춥니다.</summary>