리플레이/권한/도구 패리티 안정화: StopRequested·Paused/Resumed 반영 및 검증 보강
Some checks failed
Release Gate / gate (push) Has been cancelled

- TaskRunService에 AgentEventType.Paused/Resumed 처리 로직을 추가해 런타임 상태(일시정지/재개)가 task store에 일관되게 반영되도록 개선

- TaskRunService에 AgentEventType.StopRequested 처리 로직을 추가해 실행 중단 요청 시 agent/tool/permission/hook 범주의 run-scoped 태스크가 cancel 상태로 정리되도록 보강

- replay 복원 경로에서 StopRequested를 terminal 이벤트로 인식하도록 확장하고, TryGetScopedId/IsTerminalExecutionEvent/RemoveRunScopedActiveTasks 연계를 통해 dangling active task가 남지 않도록 수정

- OperationModePolicyTests에 Deny 모드 경계 테스트를 추가(쓰기 차단 + 읽기 허용)하여 권한 4모드 정책의 기대 동작을 명시적으로 고정

- TaskRunServiceTests에 ReplayStability 시나리오 3건 추가: (1) Paused→Resumed 후 agent active 유지, (2) StopRequested 후 dangling task 정리, (3) live StopRequested 적용 시 pending 권한/에이전트 상태 정리

- AgentParityToolsTests에 core agentic loop 도구 등록 검증 추가(file_read/write/edit, glob/grep/process, git/build/test, spawn/wait, task/todo, checkpoint/diff/suggest/tool_search/skill_manager)

- 검증 수행: dotnet build AxCopilot.sln (경고 0 / 오류 0), 대상 테스트(OperationModePolicyTests/TaskRunServiceTests/AgentParityToolsTests) 통과, ReplayStability+ParityBenchmark 필터 테스트 통과
This commit is contained in:
2026-04-03 21:35:45 +09:00
parent e7eec1035f
commit 0176754fa0
4 changed files with 191 additions and 3 deletions

View File

@@ -290,6 +290,24 @@ public sealed class TaskRunService
case Agent.AgentEventType.Planning:
StartAgentRun(evt.RunId, evt.Summary);
break;
case Agent.AgentEventType.Paused:
StartOrUpdate(
GetAgentTaskKey(evt),
"agent",
"main",
string.IsNullOrWhiteSpace(evt.Summary) ? "agent paused" : evt.Summary,
"paused",
evt.FilePath);
break;
case Agent.AgentEventType.Resumed:
StartOrUpdate(
GetAgentTaskKey(evt),
"agent",
"main",
string.IsNullOrWhiteSpace(evt.Summary) ? "agent resumed" : evt.Summary,
"running",
evt.FilePath);
break;
case Agent.AgentEventType.Complete:
CompleteAgentRun(
evt.RunId,
@@ -308,6 +326,31 @@ public sealed class TaskRunService
string.IsNullOrWhiteSpace(evt.Summary) ? "작업 완료" : evt.Summary,
"completed");
break;
case Agent.AgentEventType.StopRequested:
StartOrUpdate(
GetAgentTaskKey(evt),
"agent",
"main",
string.IsNullOrWhiteSpace(evt.Summary) ? "agent stop requested" : evt.Summary,
"running",
evt.FilePath);
CompleteAgentRun(
evt.RunId,
string.IsNullOrWhiteSpace(evt.Summary) ? "agent stop requested" : evt.Summary,
false);
CompleteByPrefix(
string.IsNullOrWhiteSpace(evt.RunId) ? "tool:" : $"tool:{evt.RunId}:",
string.IsNullOrWhiteSpace(evt.Summary) ? "agent stop requested" : evt.Summary,
"cancelled");
CompleteByPrefix(
string.IsNullOrWhiteSpace(evt.RunId) ? "permission:" : $"permission:{evt.RunId}:",
string.IsNullOrWhiteSpace(evt.Summary) ? "agent stop requested" : evt.Summary,
"cancelled");
CompleteByPrefix(
string.IsNullOrWhiteSpace(evt.RunId) ? "hook:" : $"hook:{evt.RunId}:",
string.IsNullOrWhiteSpace(evt.Summary) ? "agent stop requested" : evt.Summary,
"cancelled");
break;
case Agent.AgentEventType.Error:
if (!string.IsNullOrWhiteSpace(evt.ToolName))
{
@@ -453,7 +496,8 @@ public sealed class TaskRunService
}
if (string.Equals(item.Type, nameof(Agent.AgentEventType.Complete), StringComparison.OrdinalIgnoreCase) ||
string.Equals(item.Type, nameof(Agent.AgentEventType.Error), StringComparison.OrdinalIgnoreCase))
string.Equals(item.Type, nameof(Agent.AgentEventType.Error), StringComparison.OrdinalIgnoreCase) ||
string.Equals(item.Type, nameof(Agent.AgentEventType.StopRequested), StringComparison.OrdinalIgnoreCase))
{
return new TaskRunStore.TaskRun
{
@@ -461,7 +505,11 @@ public sealed class TaskRunService
Kind = "agent",
Title = "main",
Summary = item.Summary,
Status = string.Equals(item.Type, nameof(Agent.AgentEventType.Error), StringComparison.OrdinalIgnoreCase) ? "failed" : "completed",
Status = string.Equals(item.Type, nameof(Agent.AgentEventType.Error), StringComparison.OrdinalIgnoreCase)
? "failed"
: string.Equals(item.Type, nameof(Agent.AgentEventType.StopRequested), StringComparison.OrdinalIgnoreCase)
? "cancelled"
: "completed",
StartedAt = item.Timestamp,
UpdatedAt = item.Timestamp,
FilePath = item.FilePath,
@@ -525,6 +573,25 @@ public sealed class TaskRunService
return true;
}
if (string.Equals(item.Type, nameof(Agent.AgentEventType.Paused), StringComparison.OrdinalIgnoreCase) ||
string.Equals(item.Type, nameof(Agent.AgentEventType.Resumed), StringComparison.OrdinalIgnoreCase))
{
task = new TaskRunStore.TaskRun
{
Id = BuildScopedId("agent", item),
Kind = "agent",
Title = "main",
Summary = item.Summary,
Status = string.Equals(item.Type, nameof(Agent.AgentEventType.Paused), StringComparison.OrdinalIgnoreCase)
? "paused"
: "running",
StartedAt = item.Timestamp,
UpdatedAt = item.Timestamp,
FilePath = item.FilePath,
};
return true;
}
if (string.Equals(item.Type, nameof(Agent.AgentEventType.Thinking), StringComparison.OrdinalIgnoreCase) ||
string.Equals(item.Type, nameof(Agent.AgentEventType.Planning), StringComparison.OrdinalIgnoreCase))
{
@@ -560,6 +627,9 @@ public sealed class TaskRunService
if (string.Equals(item.Type, nameof(Agent.AgentEventType.Error), StringComparison.OrdinalIgnoreCase))
return true;
if (string.Equals(item.Type, nameof(Agent.AgentEventType.StopRequested), StringComparison.OrdinalIgnoreCase))
return true;
return false;
}
@@ -572,6 +642,7 @@ public sealed class TaskRunService
var shouldClearAllByRun =
string.Equals(item.Type, nameof(Agent.AgentEventType.Complete), StringComparison.OrdinalIgnoreCase) ||
string.Equals(item.Type, nameof(Agent.AgentEventType.StopRequested), StringComparison.OrdinalIgnoreCase) ||
(string.Equals(item.Type, nameof(Agent.AgentEventType.Error), StringComparison.OrdinalIgnoreCase)
&& string.IsNullOrWhiteSpace(item.ToolName));
@@ -608,6 +679,9 @@ public sealed class TaskRunService
if (string.Equals(item.Type, nameof(Agent.AgentEventType.Thinking), StringComparison.OrdinalIgnoreCase) ||
string.Equals(item.Type, nameof(Agent.AgentEventType.Planning), StringComparison.OrdinalIgnoreCase) ||
string.Equals(item.Type, nameof(Agent.AgentEventType.Paused), StringComparison.OrdinalIgnoreCase) ||
string.Equals(item.Type, nameof(Agent.AgentEventType.Resumed), StringComparison.OrdinalIgnoreCase) ||
string.Equals(item.Type, nameof(Agent.AgentEventType.StopRequested), StringComparison.OrdinalIgnoreCase) ||
string.Equals(item.Type, nameof(Agent.AgentEventType.Complete), StringComparison.OrdinalIgnoreCase))
return BuildScopedId("agent", item);
@@ -698,3 +772,7 @@ public sealed class TaskRunService
return delta <= TimeSpan.FromSeconds(2);
}
}