[Phase 17-C] 훅 시스템 고도화 — 11종 이벤트 연결 + Prompt 모드 구현

AppSettings.AgentConfig.cs:
- ExtendedHooksConfig에 4개 이벤트 추가:
  preToolUse, postToolUse, postToolUseFailure, agentStop

AgentLoopService.ExtendedHooks.cs (신규, 150줄):
- RunExtendedEventAsync(): 이벤트 훅 실행, 결과(additionalContext) 시스템 메시지 주입, HookFired 이벤트 로그
- GetExtendedHooks(): 설정에서 HookEventKind별 런타임 엔트리 조회
- ConvertToRuntimeEntries(): ExtendedHookEntryConfig → ExtendedHookEntry 변환
- ApplyExtendedHookResult(): additionalContext in-place 주입 (marker 기반 교체)

ExtendedHookRunner.cs:
- RunEventAsync() / ExecuteSingleAsync()에 LlmService? llm 파라미터 추가
- RunPromptHookAsync() 신규: {{tool_name/input/output/user_message/event/session_id}} 치환
  → LLM 호출 → block/deny/차단 감지 시 Block=true 반환
- using AxCopilot.Models 추가

AgentLoopService.cs:
- SessionStart 훅 (fire-and-forget, TaskState init 직후)
- UserPromptSubmit 훅 (동기, Block=true 시 즉시 반환 "⚠ 훅 정책에 의해 차단")
- PreCompact 훅 (적극적 압축 직전, 동기)
- PostCompact 훅 × 2 (기본/적극적 압축 완료 후, fire-and-forget)
- SessionEnd + AgentStop 훅 (finally 블록, fire-and-forget)

AgentLoopService.Execution.cs:
- RunToolHooksAsync() 개선: 레거시 AgentHookRunner 유지
  + ExtendedHookRunner PreToolUse/PostToolUse/PostToolUseFailure 추가
이벤트 커버리지: 11종 완전 연결. Agent 모드는 Phase 18-A 예정.
빌드: 경고 0, 오류 0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 00:14:19 +09:00
parent 2383b1e220
commit 0a58419c8a
6 changed files with 327 additions and 28 deletions

View File

@@ -176,6 +176,23 @@ public partial class AgentLoopService
_ = _eventLog?.AppendAsync(AgentEventLogType.UserMessage,
JsonSerializer.Serialize(new { length = userQuery.Length }));
await InitTaskStateAsync(userQuery, _sessionId);
// Phase 17-C: SessionStart 훅 실행 (fire-and-forget)
_ = RunExtendedEventAsync(HookEventKind.SessionStart, messages, CancellationToken.None,
userMessage: userQuery);
// Phase 17-C: UserPromptSubmit 훅 실행 — 차단 시 즉시 종료
if (!string.IsNullOrWhiteSpace(userQuery))
{
var promptBlocked = await RunExtendedEventAsync(
HookEventKind.UserPromptSubmit, messages, ct, userMessage: userQuery);
if (promptBlocked)
{
EmitEvent(AgentEventType.Complete, "", "훅 정책에 의해 요청이 차단되었습니다");
return "⚠ 요청이 훅 정책에 의해 차단되었습니다.";
}
}
var consecutiveErrors = 0; // Self-Reflection: 연속 오류 카운터
var totalToolCalls = 0; // 복잡도 추정용
@@ -357,6 +374,8 @@ public partial class AgentLoopService
_ = _eventLog?.AppendAsync(AgentEventLogType.CompactionCompleted,
JsonSerializer.Serialize(new { messageCount = messages.Count }));
UpdateTaskStateSummaryAsync("컨텍스트 압축 완료");
// Phase 17-C: PostCompact 훅 (fire-and-forget)
_ = RunExtendedEventAsync(HookEventKind.PostCompact, messages, CancellationToken.None);
}
// 임계치 기반 적극적 압축 (이전 LLM 호출의 토큰 사용량 기준)
@@ -374,6 +393,8 @@ public partial class AgentLoopService
$"⚠ 컨텍스트 사용량 {usagePct}% — 적극적 컴팩션 실행");
_ = _eventLog?.AppendAsync(AgentEventLogType.CompactionTriggered,
JsonSerializer.Serialize(new { aggressive = true, usagePct }));
// Phase 17-C: PreCompact 훅 — 적극적 압축 직전 (압축 발생 확실)
await RunExtendedEventAsync(HookEventKind.PreCompact, messages, ct);
var targetTokens = (int)(llm.MaxContextTokens * Defaults.ContextCompressionTargetRatio);
var compacted = await ContextCondenser.CondenseIfNeededAsync(
messages, _llm, targetTokens, ct);
@@ -383,6 +404,8 @@ public partial class AgentLoopService
_ = _eventLog?.AppendAsync(AgentEventLogType.CompactionCompleted,
JsonSerializer.Serialize(new { aggressive = true, messageCount = messages.Count }));
UpdateTaskStateSummaryAsync($"적극적 컴팩션 완료 (이전 컨텍스트 사용량 {usagePct}%)");
// Phase 17-C: PostCompact 훅 (fire-and-forget)
_ = RunExtendedEventAsync(HookEventKind.PostCompact, messages, CancellationToken.None);
}
}
}
@@ -778,6 +801,10 @@ public partial class AgentLoopService
}
finally
{
// Phase 17-C: SessionEnd + AgentStop 확장 훅 실행 (fire-and-forget)
_ = RunExtendedEventAsync(HookEventKind.SessionEnd, null, CancellationToken.None, userMessage: userQuery);
_ = RunExtendedEventAsync(HookEventKind.AgentStop, null, CancellationToken.None, userMessage: userQuery);
// 세션 종료 이벤트 기록
if (_eventLog != null)
_ = _eventLog.AppendAsync(AgentEventLogType.SessionEnd,