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>
196 lines
7.6 KiB
JavaScript
196 lines
7.6 KiB
JavaScript
// 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]);
|
||
}
|