Files
ClawdDotNet/src/ClawdDotNet.App/Services/RunInsights.cs
T
RichardandClaude Opus 5.5 f812dbd856 Verweis statt Nacherzaehlung: die Engine stempelt den Ursprung (#697, #677)
- 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>
2026-09-25 09:27:47 +02:00

75 lines
3.0 KiB
C#

using ClawdDotNet.Core.Analytics;
using ClawdDotNet.Core.Audit;
using ClawdDotNet.Core.Staging;
using ClawdDotNet.Core.Tasks;
using ClawdDotNet.Core.Transcripts;
namespace ClawdDotNet.App.Services;
/// <summary>Everything known about a single run (#660).</summary>
public sealed record RunDetail(
string RunId,
RunReceipt? Receipt,
string? TaskTitle,
IReadOnlyList<AuditEntry> ToolCalls,
IReadOnlyList<StagedCall> Approvals,
RunTranscript? Transcript,
string? SystemPrompt,
string TranscriptState,
// #697: The runs this one delegated to — the way down to the originals.
IReadOnlyList<RunReceipt>? CalledRuns = null);
/// <summary>
/// Read side of the usage analysis and the prompt history (#660) for both user interfaces.
/// Owns no data: receipts and audit come from the audit repository, transcripts from the
/// transcript store, task titles from the board.
/// </summary>
public sealed class RunInsights(
IAuditRepository audit,
IRunTranscriptStore? transcripts,
TaskboardService? board,
StagingService? staging,
Func<string, int> retentionFor)
{
public UsageAnalytics Analytics { get; } = new(audit, board is null
? null
: async (id, ct) => (await board.GetAsync(id, ct))?.Title);
public IRunTranscriptStore? Transcripts => transcripts;
public async Task<RunDetail?> GetRunAsync(string runId, CancellationToken ct)
{
var receipt = await audit.GetReceiptForRunAsync(runId, ct);
var calls = await audit.ListForRunAsync(runId, ct);
var transcript = transcripts is null ? null : await transcripts.GetAsync(runId, ct);
if (receipt is null && calls.Count == 0 && transcript is null)
return null;
var agentId = receipt?.AgentId ?? transcript?.AgentId ?? calls.FirstOrDefault()?.AgentId ?? "";
var taskId = receipt?.TaskId ?? transcript?.TaskId;
return new RunDetail(
runId,
receipt,
taskId is not null && board is not null ? (await board.GetAsync(taskId, ct))?.Title : null,
// Only the completed calls; the "started" markers (#554) would double every line.
calls.Where(c => c.Status != AuditStatus.Started).ToList(),
staging is null ? [] : await staging.ListForRunAsync(runId, ct),
transcript,
transcript?.SystemPromptHash is { } hash && transcripts is not null
? await transcripts.GetSystemPromptAsync(hash, ct)
: null,
TranscriptState(transcript, agentId),
await audit.ListReceiptsCalledFromAsync(runId, ct));
}
/// <summary>Why there is or is not a transcript - the UI says it instead of showing an empty box.</summary>
private string TranscriptState(RunTranscript? transcript, string agentId)
{
if (transcript is not null) return "recorded";
if (transcripts is null) return "unavailable";
return retentionFor(agentId) == PromptHistorySettings.Never ? "disabled" : "missing";
}
}