- Werkzeugkontext kennt Lauf und Aufgabe des Laufs (RunId, RunTaskId); die Delegationskette traegt den Lauf des Aufrufers weiter (RunId, CallerRunId) - Aufgaben tragen origin_run / origin_task (Datei, DB, Taskboard "Arose from"), gestempelt, nie vom Agenten behauptet - Kommentare nennen den Lauf, in dem sie geschrieben wurden - Laufbeleg: CallerRunId; Laufansicht fuehrt zu "Delegiert von" und "Delegiert an" - #677: task_update ersetzt den Rumpf nicht mehr, Abnahmekriterien aendert nur der Ersteller; Kommentare der Fremdinhalt-Stufe auf fremden Aufgaben kommen gerahmt an - WebUI: Herkunft auf der Aufgabenkarte mit Link zum Lauf Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
103 lines
4.6 KiB
C#
103 lines
4.6 KiB
C#
using System.Collections.Concurrent;
|
|
using ClawdDotNet.Core.Audit;
|
|
|
|
namespace ClawdDotNet.Core.Tests.Infrastructure;
|
|
|
|
/// <summary>
|
|
/// In-Memory-Attrappe des Audit-Logs für Engine-Tests — hält Einträge und Belege in
|
|
/// Listen, damit ein Test prüfen kann, was die Engine stempelt, ohne eine Datei.
|
|
/// </summary>
|
|
internal sealed class InMemoryAuditRepository : IAuditRepository
|
|
{
|
|
public ConcurrentQueue<AuditEntry> Entries { get; } = new();
|
|
public ConcurrentQueue<RunReceipt> Receipts { get; } = new();
|
|
|
|
public Task AppendAsync(AuditEntry entry, CancellationToken ct)
|
|
{
|
|
Entries.Enqueue(entry);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task RecordReceiptAsync(RunReceipt receipt, CancellationToken ct)
|
|
{
|
|
Receipts.Enqueue(receipt);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task<IReadOnlyList<AuditEntry>> ListRecentAsync(int limit, CancellationToken ct)
|
|
=> Task.FromResult<IReadOnlyList<AuditEntry>>(Entries.Reverse().Take(limit).ToList());
|
|
|
|
public Task<IReadOnlyList<AuditEntry>> ListForRunAsync(string runId, CancellationToken ct)
|
|
=> Task.FromResult<IReadOnlyList<AuditEntry>>(Entries.Where(e => e.RunId == runId).ToList());
|
|
|
|
public Task<RunReceipt?> GetReceiptForRunAsync(string runId, CancellationToken ct)
|
|
=> Task.FromResult(Receipts.LastOrDefault(r => r.RunId == runId));
|
|
|
|
public Task<IReadOnlyList<RunReceipt>> ListReceiptsCalledFromAsync(string runId, CancellationToken ct)
|
|
=> Task.FromResult<IReadOnlyList<RunReceipt>>(Receipts.Where(r => r.CallerRunId == runId).ToList());
|
|
|
|
public Task<IReadOnlyList<RunReceipt>> ListReceiptsForTaskAsync(string taskId, CancellationToken ct)
|
|
=> Task.FromResult<IReadOnlyList<RunReceipt>>(Receipts.Where(r => r.TaskId == taskId).ToList());
|
|
|
|
public Task<IReadOnlyList<RunReceipt>> ListReceiptsAsync(DateTime fromUtc, DateTime toUtc, int limit, CancellationToken ct)
|
|
=> Task.FromResult<IReadOnlyList<RunReceipt>>(Receipts
|
|
.Where(r => r.OccurredAt >= fromUtc && r.OccurredAt < toUtc)
|
|
.OrderBy(r => r.OccurredAt).Take(limit).ToList());
|
|
|
|
public Task<int> CountAsync(CancellationToken ct) => Task.FromResult(Entries.Count);
|
|
|
|
/// <summary>Spiegelt die SQL-Regel: Started ohne späteren Abschluss derselben CallId.</summary>
|
|
public Task<IReadOnlyList<AuditEntry>> ListOpenCallsAsync(CancellationToken ct)
|
|
{
|
|
var alle = Entries.ToList();
|
|
|
|
var abgeschlossen = alle
|
|
.Where(e => e.Status != AuditStatus.Started && e.CallId.Length > 0)
|
|
.Select(e => e.CallId)
|
|
.ToHashSet();
|
|
|
|
var offen = alle
|
|
.Where(e => e.Status == AuditStatus.Started
|
|
&& e.CallId.Length > 0
|
|
&& !abgeschlossen.Contains(e.CallId))
|
|
.ToList();
|
|
|
|
return Task.FromResult<IReadOnlyList<AuditEntry>>(offen);
|
|
}
|
|
|
|
public Task<IReadOnlyList<AuditEntry>> ListForAgentAsync(
|
|
string agentId, DateTime? seit, int limit, CancellationToken ct)
|
|
=> Task.FromResult<IReadOnlyList<AuditEntry>>(
|
|
Entries.Where(e => e.AgentId == agentId)
|
|
.Where(e => seit is null || e.OccurredAt >= seit)
|
|
.Reverse()
|
|
.Take(limit)
|
|
.ToList());
|
|
|
|
public Task<IReadOnlyList<ToolUsageStat>> SummarizeToolUsageAsync(
|
|
string? agentId, DateTime? seit, CancellationToken ct)
|
|
=> Task.FromResult<IReadOnlyList<ToolUsageStat>>(
|
|
Entries.Where(e => agentId is null || e.AgentId == agentId)
|
|
.Where(e => seit is null || e.OccurredAt >= seit)
|
|
.GroupBy(e => (e.AgentId, e.Tool))
|
|
.Select(g =>
|
|
{
|
|
var abgeschlossen = g.Where(e => e.Status != AuditStatus.Started).ToList();
|
|
var begonnen = g.Count(e => e.Status == AuditStatus.Started);
|
|
|
|
return new ToolUsageStat
|
|
{
|
|
AgentId = g.Key.AgentId,
|
|
Tool = g.Key.Tool,
|
|
Calls = abgeschlossen.Count,
|
|
Failed = abgeschlossen.Count(e => e.Status == AuditStatus.Error),
|
|
Denied = abgeschlossen.Count(e => e.Status == AuditStatus.Denied),
|
|
Unfinished = Math.Max(0, begonnen - abgeschlossen.Count),
|
|
TotalDurationMs = abgeschlossen.Sum(e => e.DurationMs),
|
|
MaxDurationMs = g.Max(e => e.DurationMs)
|
|
};
|
|
})
|
|
.OrderByDescending(s => s.Calls)
|
|
.ToList());
|
|
}
|