Prompt History und Verbrauchsauswertung (#660), englische Hauptsprache (#661)

Prompt history: every run records its transcript (task, each model step with
tokens and cost, tool calls and results, engine follow-ups) - bounded in size,
scrubbed of recognisable secrets, system prompts stored once per text.
Retention in days per instance (default 14) and per agent (0 = never, -1 =
forever); an hourly janitor deletes what is past it, "never" also purges.

Usage analysis over run receipts: filter by period, agent, model, event, status,
cost and free text; sort by every column; totals over the whole filter; hourly
or daily series grouped by agent, model or event; markers for budget denials and
model changes; run detail with receipt, transcript, tool calls and approvals.

WebUI: Analysis > Token usage > Runs / Timeline, run detail, Prompt History with
retention settings. New code, comments and texts in English; texts come from
language packs (i18n/en.json, de.json) via modules/i18n.js. CLAUDE.md records
English as the main language.

Version 0.1.12.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-19 15:33:09 +02:00
co-authored by Claude Opus 5
parent 2f0361278b
commit 5adf4c1ca6
40 changed files with 3521 additions and 10 deletions
@@ -26,7 +26,7 @@ internal sealed class EngineFixture
public EngineFixture(
IAuditRepository? audit = null, StagingGate? staging = null, TimeProvider? clock = null,
IUsageRepository? usage = null)
IUsageRepository? usage = null, Core.Transcripts.IRunTranscriptStore? transcripts = null)
{
Engine = new AgentEngine(
Client,
@@ -37,7 +37,10 @@ internal sealed class EngineFixture
usageRepository: usage,
auditRepository: audit,
stagingGate: staging,
clock: clock);
clock: clock)
{
TranscriptStore = transcripts
};
// Kein Agent-Verzeichnis → keine Persistenz auf die Platte während der Tests.
Engine.SetAgentConfigProvider(() => _agents, "test-instance", _ => null);
@@ -36,6 +36,11 @@ internal sealed class InMemoryAuditRepository : IAuditRepository
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>
@@ -20,6 +20,9 @@ internal sealed class InMemoryStagingRepository : IStagingRepository
public Task<StagedCall?> GetAsync(long id, CancellationToken ct)
=> Task.FromResult(_calls.GetValueOrDefault(id));
public Task<IReadOnlyList<StagedCall>> ListForRunAsync(string runId, CancellationToken ct)
=> Task.FromResult<IReadOnlyList<StagedCall>>(_calls.Values.Where(c => c.RunId == runId).OrderBy(c => c.Id).ToList());
public Task<int> CountExecutedAsync(string agentId, string tool, string? action, DateTime since, CancellationToken ct)
=> Task.FromResult(_calls.Values.Count(c =>
c.Status == StagingStatus.Executed && c.AgentId == agentId && c.Tool == tool