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
@@ -0,0 +1,195 @@
// ClawdDotNet — run detail: prompt, response and every step in between (#660)
//
// Reached from Token usage › Runs and from Prompt History. Shows the receipt, the recorded
// transcript (task, each model step with its tokens and cost, each tool call and result),
// the system prompt, and what the run proposed for approval.
//
// Everything in a transcript is foreign text - model output, web pages, database rows.
// It is only ever inserted via textContent.
import { api } from "../modules/api.js";
import {
erstelleElement,
erstelleKarte,
erstellePlakette,
formatiereDatumZeit,
formatiereTokens,
formatiereKosten
} from "../modules/dom.js";
import { t } from "../modules/i18n.js";
import { statusBadge, sourceLabel, formatDuration } from "./usage-runs.js";
export async function renderRunDetail(runId, backPath) {
const container = document.getElementById("ansicht-inhalt");
if (!container) return;
const back = erstelleElement("a", { href: `#${backPath}`, klasse: "zurueck-link", text: t("detail.back") });
// Back to the filtered list if we came from one.
back.addEventListener("click", (event) => {
if (window.history.length > 1) {
event.preventDefault();
window.history.back();
}
});
const layout = erstelleElement("div", { klasse: "verbrauch-ansicht lauf-detail" }, back,
erstelleElement("h1", { text: t("detail.title") }),
erstelleElement("div", { klasse: "hinweis-zentriert", text: t("common.loading") }));
container.replaceChildren(layout);
let run;
try {
run = await api(`/runs/${encodeURIComponent(runId)}`);
} catch (err) {
layout.lastChild.replaceWith(erstelleKarte(t("detail.notFound"), [
erstelleElement("p", { klasse: "gedaempft", text: err.message })
]));
return;
}
const parts = [buildReceipt(run)];
parts.push(run.transcript ? buildTranscript(run) : buildTranscriptState(run));
if (!run.transcript && run.toolCalls.length > 0) parts.push(buildToolCalls(run.toolCalls));
if (run.approvals.length > 0) parts.push(buildApprovals(run.approvals));
layout.lastChild.replaceWith(...parts);
}
function buildReceipt(run) {
const r = run.receipt;
const list = erstelleElement("dl", { klasse: "datenliste" });
const add = (label, value) => list.append(
erstelleElement("dt", { text: t(label) }),
value instanceof Node ? erstelleElement("dd", {}, value) : erstelleElement("dd", { text: value ?? "—" }));
add("detail.runId", run.runId);
if (r) {
add("runs.col.time", formatiereDatumZeit(r.occurredAt));
add("runs.col.agent", r.agentId);
add("runs.col.model", r.model);
add("runs.col.event", r.taskTitle ? `${sourceLabel(r.source)} · ${r.taskTitle}` : sourceLabel(r.source));
add("runs.col.status", statusBadge(r.status));
add("runs.col.steps", String(r.stepCount));
add("detail.tokens", t("detail.tokensValue", {
prompt: formatiereTokens(r.promptTokens),
completion: formatiereTokens(r.completionTokens),
cached: formatiereTokens(r.cachedTokens)
}));
add("runs.col.cost", r.costIsKnown || r.costUsd > 0 ? formatiereKosten(r.costUsd) : "—");
add("runs.col.duration", formatDuration(r.durationMs));
if (r.result) add("detail.result", r.result);
}
return erstelleKarte(t("detail.receipt"), [list]);
}
function buildTranscriptState(run) {
const text = {
disabled: t("detail.transcript.disabled"),
unavailable: t("detail.transcript.unavailable"),
missing: t("detail.transcript.missing")
}[run.transcriptState] ?? t("detail.transcript.missing");
return erstelleKarte(t("detail.transcript"), [erstelleElement("p", { klasse: "gedaempft", text })]);
}
function pre(text) {
return erstelleElement("pre", { klasse: "code-block transkript-text", text: text ?? "" });
}
function prettyJson(text) {
if (!text) return "";
try {
return JSON.stringify(JSON.parse(text), null, 2);
} catch {
return text;
}
}
function buildTranscript(run) {
const transcript = run.transcript;
const items = [];
if (run.systemPrompt) {
const details = erstelleElement("details", { klasse: "transkript-system" },
erstelleElement("summary", { text: t("detail.systemPrompt", { n: run.systemPrompt.length }) }),
pre(run.systemPrompt));
items.push(details);
}
if (transcript.truncated) {
items.push(erstelleElement("div", { klasse: "hinweis-box warnung", text: t("detail.transcript.truncated") }));
}
let step = 0;
for (const entry of transcript.entries) {
const kind = entry.kind;
const block = erstelleElement("div", { klasse: `transkript-eintrag art-${kind}` });
const head = erstelleElement("div", { klasse: "transkript-kopf" });
if (kind === "assistant") {
step++;
head.append(erstelleElement("strong", { text: t("detail.entry.assistant", { n: step }) }));
if (entry.promptTokens !== undefined && entry.promptTokens !== null) {
head.append(erstellePlakette(t("detail.entry.usage", {
prompt: formatiereTokens(entry.promptTokens),
completion: formatiereTokens(entry.completionTokens ?? 0)
}), "neutral"));
}
if (entry.costUsd) head.append(erstellePlakette(formatiereKosten(entry.costUsd), "neutral"));
if (entry.finishReason && entry.finishReason !== "stop" && entry.finishReason !== "tool_calls") {
head.append(erstellePlakette(entry.finishReason, "warnung"));
}
} else {
head.append(erstelleElement("strong", {
text: kind === "tool_result"
? t("detail.entry.tool_result", { tool: entry.toolName ?? "?" })
: t(`detail.entry.${kind}`)
}));
}
head.append(erstelleElement("span", { klasse: "gedaempft klein", text: formatiereDatumZeit(entry.at) }));
if (entry.clipped) head.append(erstellePlakette(t("detail.entry.clipped"), "warnung"));
block.append(head);
if (entry.text) block.append(pre(entry.text));
for (const call of entry.toolCalls ?? []) {
block.append(erstelleElement("div", { klasse: "transkript-aufruf" },
erstelleElement("span", { klasse: "klein", text: t("detail.entry.call", { tool: call.name }) }),
pre(prettyJson(call.arguments))));
}
items.push(block);
}
const summary = t("detail.transcript.size", { n: transcript.entries.length, chars: formatiereTokens(transcript.sizeChars) });
return erstelleKarte(t("detail.transcript"), [erstelleElement("p", { klasse: "hinweis", text: summary }), ...items]);
}
function buildToolCalls(calls) {
const table = erstelleElement("table", { klasse: "daten-tabelle" });
const head = erstelleElement("tr", {});
for (const label of ["runs.col.time", "detail.tool", "runs.col.status", "detail.arguments"]) {
head.append(erstelleElement("th", { text: t(label) }));
}
table.append(head);
for (const call of calls) {
table.append(erstelleElement("tr", {},
erstelleElement("td", { text: formatiereDatumZeit(call.at) }),
erstelleElement("td", { text: call.tool }),
erstelleElement("td", { text: call.status }),
erstelleElement("td", {}, pre(prettyJson(call.arguments)))));
}
return erstelleKarte(t("detail.toolCalls"), [
erstelleElement("p", { klasse: "hinweis", text: t("detail.toolCalls.hint") }),
erstelleElement("div", { klasse: "tabelle-scroll" }, table)
]);
}
function buildApprovals(approvals) {
const list = erstelleElement("ul", { klasse: "schlichte-liste" });
for (const a of approvals) {
list.append(erstelleElement("li", {},
erstelleElement("strong", { text: `#${a.id} ${a.tool}.${a.action ?? "?"} ` }),
erstellePlakette(a.status, a.status === "Executed" ? "erfolg" : a.status === "Pending" ? "warnung" : "neutral")));
}
return erstelleKarte(t("detail.approvals"), [list]);
}