feat(reports): record personal report generation telemetry
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled

This commit is contained in:
Jesse_Chen
2026-08-30 20:01:19 +08:00
parent f9e9b03a7e
commit 1f1fdb1d15
2 changed files with 106 additions and 20 deletions
+48 -20
View File
@@ -122,6 +122,7 @@ export type PersonalReportAgentTelemetry = Readonly<{
inputTokens: number | null;
outputTokens: number | null;
totalTokens: number | null;
finishReason: string | null;
repairAttempted: boolean;
}>;
@@ -142,18 +143,32 @@ Structure rules:
- Keep actions concrete and cautious; caveats must state limits honestly.
- Write formal, readable Simplified Chinese for a printed report.`;
function readUsage(value: unknown) {
const record = value !== null && typeof value === "object" && !Array.isArray(value)
? value as Record<string, unknown>
: {};
const numberOrNull = (key: string) => (
typeof record[key] === "number" && Number.isFinite(record[key]) ? record[key] as number : null
);
return {
inputTokens: numberOrNull("inputTokens"),
outputTokens: numberOrNull("outputTokens"),
totalTokens: numberOrNull("totalTokens"),
};
async function readUsage(value: unknown) {
try {
const resolved = await Promise.resolve(value);
const record = resolved !== null && typeof resolved === "object" && !Array.isArray(resolved)
? resolved as Record<string, unknown>
: {};
const numberOrNull = (key: string) => (
typeof record[key] === "number" && Number.isFinite(record[key]) ? record[key] as number : null
);
return {
inputTokens: numberOrNull("inputTokens"),
outputTokens: numberOrNull("outputTokens"),
totalTokens: numberOrNull("totalTokens"),
};
} catch {
return { inputTokens: null, outputTokens: null, totalTokens: null };
}
}
async function readFinishReason(value: unknown): Promise<string | null> {
try {
const resolved = await Promise.resolve(value);
return typeof resolved === "string" && resolved.length <= 80 ? resolved : null;
} catch {
return null;
}
}
export class PersonalReportAgentOutputError extends Error {
@@ -221,12 +236,13 @@ export function createPersonalReportAgent(model: ResolvedLanguageModel): ReportA
};
let repairAttempted = false;
type GenerationResult = { object?: unknown; usage?: unknown; finishReason?: unknown };
const runOnce = (content: string) => agent.generate(
[{ role: "user", content }],
{ abortSignal: signal, structuredOutput },
);
const accept = (result: { object?: unknown; usage?: unknown }):
let attemptReturned = false;
const accept = (result: GenerationResult):
| { ok: true; data: PersonalReportAgentOutput }
| { ok: false; cause: "schema" | "bind"; error?: unknown } => {
const parsed = personalReportAgentOutputSchema.safeParse(result.object);
@@ -241,22 +257,27 @@ export function createPersonalReportAgent(model: ResolvedLanguageModel): ReportA
};
try {
attemptReturned = false;
const first = await runOnce(prompt);
attemptReturned = true;
const firstAccepted = accept(first);
if (firstAccepted.ok) {
logTelemetry(model.id, startedAt, false, "resolved", first.usage);
await logTelemetry(model.id, startedAt, false, "resolved", first.usage, first.finishReason);
return firstAccepted.data;
}
await logTelemetry(model.id, startedAt, false, "failed", first.usage, first.finishReason);
// Exactly one repair retry is allowed. A second failure is terminal.
repairAttempted = true;
attemptReturned = false;
const repaired = await runOnce(`${prompt}${REPAIR_PROMPT_SUFFIX}`);
attemptReturned = true;
const repairedAccepted = accept(repaired);
if (repairedAccepted.ok) {
logTelemetry(model.id, startedAt, true, "resolved", repaired.usage);
await logTelemetry(model.id, startedAt, true, "resolved", repaired.usage, repaired.finishReason);
return repairedAccepted.data;
}
logTelemetry(model.id, startedAt, true, "failed", repaired.usage);
await logTelemetry(model.id, startedAt, true, "failed", repaired.usage, repaired.finishReason);
if (repairedAccepted.cause === "bind" && repairedAccepted.error) {
throw repairedAccepted.error;
}
@@ -265,21 +286,27 @@ export function createPersonalReportAgent(model: ResolvedLanguageModel): ReportA
if (error instanceof PersonalReportAgentOutputError) throw error;
if (isAbortError(error, signal)) throw error;
if (error instanceof Error && error.message.startsWith("report_writer_")) throw error;
logTelemetry(model.id, startedAt, repairAttempted, "failed", null);
if (!attemptReturned) {
await logTelemetry(model.id, startedAt, repairAttempted, "failed", null, null);
}
throw error;
}
},
};
}
function logTelemetry(
async function logTelemetry(
modelId: string,
startedAt: number,
repairAttempted: boolean,
outcome: "resolved" | "failed",
usage: unknown,
finishReason: unknown,
) {
const tokens = readUsage(usage);
const [tokens, finishReasonValue] = await Promise.all([
readUsage(usage),
readFinishReason(finishReason),
]);
const telemetry: PersonalReportAgentTelemetry = {
modelId,
outcome,
@@ -287,6 +314,7 @@ function logTelemetry(
inputTokens: tokens.inputTokens,
outputTokens: tokens.outputTokens,
totalTokens: tokens.totalTokens,
finishReason: finishReasonValue,
repairAttempted,
};
// Telemetry must never include the prompt, the packet, birth data or the