feat(agent): harden loop recovery and permission hook lifecycle
Some checks failed
Release Gate / gate (push) Has been cancelled
Some checks failed
Release Gate / gate (push) Has been cancelled
This commit is contained in:
@@ -168,13 +168,11 @@ public sealed class TaskRunService
|
||||
var restored = new List<TaskRunStore.TaskRun>();
|
||||
var active = new Dictionary<string, TaskRunStore.TaskRun>(StringComparer.OrdinalIgnoreCase);
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var normalizedHistory = NormalizeReplayHistory(history);
|
||||
|
||||
if (history != null)
|
||||
if (normalizedHistory.Count > 0)
|
||||
{
|
||||
foreach (var item in history
|
||||
.OrderBy(x => x.Timestamp)
|
||||
.ThenBy(x => x.Iteration)
|
||||
.ThenBy(x => x.ElapsedMs))
|
||||
foreach (var item in normalizedHistory)
|
||||
{
|
||||
var scopedId = TryGetScopedId(item);
|
||||
if (string.IsNullOrWhiteSpace(scopedId))
|
||||
@@ -193,7 +191,7 @@ public sealed class TaskRunService
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var item in history
|
||||
foreach (var item in normalizedHistory
|
||||
.OrderByDescending(x => x.Timestamp)
|
||||
.ThenByDescending(GetReplayPriority)
|
||||
.ThenByDescending(x => x.Iteration)
|
||||
@@ -511,6 +509,22 @@ public sealed class TaskRunService
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.Equals(item.Type, nameof(Agent.AgentEventType.SkillCall), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
task = new TaskRunStore.TaskRun
|
||||
{
|
||||
Id = BuildScopedId("tool", item),
|
||||
Kind = "skill",
|
||||
Title = string.IsNullOrWhiteSpace(item.ToolName) ? "skill" : item.ToolName,
|
||||
Summary = item.Summary,
|
||||
Status = "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))
|
||||
{
|
||||
@@ -588,6 +602,7 @@ public sealed class TaskRunService
|
||||
return BuildScopedId("permission", item);
|
||||
|
||||
if (string.Equals(item.Type, nameof(Agent.AgentEventType.ToolCall), StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(item.Type, nameof(Agent.AgentEventType.SkillCall), StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(item.Type, nameof(Agent.AgentEventType.ToolResult), StringComparison.OrdinalIgnoreCase))
|
||||
return BuildScopedId("tool", item);
|
||||
|
||||
@@ -613,4 +628,73 @@ public sealed class TaskRunService
|
||||
? $"{prefix}:{suffix}"
|
||||
: $"{prefix}:{evt.RunId}:{suffix}";
|
||||
}
|
||||
|
||||
private static IReadOnlyList<Models.ChatExecutionEvent> NormalizeReplayHistory(
|
||||
IEnumerable<Models.ChatExecutionEvent>? history)
|
||||
{
|
||||
if (history == null)
|
||||
return Array.Empty<Models.ChatExecutionEvent>();
|
||||
|
||||
var ordered = history
|
||||
.Where(x => x != null)
|
||||
.OrderBy(x => x.Timestamp == default ? DateTime.MinValue : x.Timestamp)
|
||||
.ThenBy(x => x.Iteration)
|
||||
.ThenBy(x => x.ElapsedMs)
|
||||
.ToList();
|
||||
|
||||
var result = new List<Models.ChatExecutionEvent>(ordered.Count);
|
||||
foreach (var item in ordered)
|
||||
{
|
||||
if (result.Count == 0)
|
||||
{
|
||||
result.Add(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
var last = result[^1];
|
||||
if (!IsNearDuplicateReplayEvent(last, item))
|
||||
{
|
||||
result.Add(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
last.Timestamp = item.Timestamp;
|
||||
last.ElapsedMs = Math.Max(last.ElapsedMs, item.ElapsedMs);
|
||||
last.InputTokens = Math.Max(last.InputTokens, item.InputTokens);
|
||||
last.OutputTokens = Math.Max(last.OutputTokens, item.OutputTokens);
|
||||
last.StepCurrent = Math.Max(last.StepCurrent, item.StepCurrent);
|
||||
last.StepTotal = Math.Max(last.StepTotal, item.StepTotal);
|
||||
last.Iteration = Math.Max(last.Iteration, item.Iteration);
|
||||
if (string.IsNullOrWhiteSpace(last.FilePath))
|
||||
last.FilePath = item.FilePath;
|
||||
if (!string.IsNullOrWhiteSpace(item.Summary) && item.Summary.Length >= (last.Summary?.Length ?? 0))
|
||||
last.Summary = item.Summary;
|
||||
if (item.Steps is { Count: > 0 })
|
||||
last.Steps = item.Steps.ToList();
|
||||
if (!string.IsNullOrWhiteSpace(item.ToolInput))
|
||||
last.ToolInput = item.ToolInput;
|
||||
last.Success = last.Success && item.Success;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool IsNearDuplicateReplayEvent(Models.ChatExecutionEvent left, Models.ChatExecutionEvent right)
|
||||
{
|
||||
if (!string.Equals(left.RunId, right.RunId, StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
if (!string.Equals(left.Type, right.Type, StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
if (!string.Equals(left.ToolName, right.ToolName, StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
if (!string.Equals(left.Summary?.Trim(), right.Summary?.Trim(), StringComparison.Ordinal))
|
||||
return false;
|
||||
if (!string.Equals(left.FilePath ?? "", right.FilePath ?? "", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
if (left.Success != right.Success)
|
||||
return false;
|
||||
|
||||
var delta = (right.Timestamp - left.Timestamp).Duration();
|
||||
return delta <= TimeSpan.FromSeconds(2);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user