Files
Jyotisha/frontend/src/lib/agent-observability.ts
T
Jesse_Chen 7d667fecbf fix(consult): treat pinched answers as failed and restore reply actions
Incomplete Flash generations were billed as completed consultations. Fail
those runs, keep the partial text, and reuse the rectification like/copy/rerun
bar on ordinary chat replies.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-19 19:40:50 +08:00

198 lines
6.7 KiB
TypeScript

import { z } from "zod";
import { consultationDomainSchema } from "./consultation-domain-registry.ts";
/**
* Closed, non-PII observability contract for Agent runs.
*
* There is intentionally no free-form metadata bag. Raw input/output text,
* prompts, messages, birth data, names, email addresses, secrets, API keys,
* provider payloads, stack traces and internal filesystem paths are not fields
* in this schema. Every object is strict, so unknown fields fail closed.
*/
const opaqueIdSchema = z.string()
.min(1)
.max(128)
.regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/, "invalid opaque identifier");
const machineValueSchema = z.string()
.min(1)
.max(128)
.regex(/^[A-Za-z0-9][A-Za-z0-9._:+-]*$/, "invalid machine value");
const machineCodeSchema = z.string()
.min(1)
.max(80)
.regex(/^[a-z][a-z0-9._-]*$/, "invalid machine code");
const durationMsSchema = z.number().int().min(0).max(7 * 24 * 60 * 60 * 1000);
const countSchema = z.number().int().min(0).max(1_000_000);
const tokenCountSchema = z.number().int().min(0).max(1_000_000_000);
export const agentObservabilityStepStatuses = [
"completed",
"failed",
"cancelled",
"skipped",
] as const;
export const billingSettlementResults = [
"completed",
"cancelled",
"failed",
"not_applicable",
] as const;
export type AgentBillingSettlementResult = (typeof billingSettlementResults)[number];
export type AgentSettlementResult = Exclude<AgentBillingSettlementResult, "not_applicable">;
/**
* Closed vocabulary for why the model stopped stepping. These are the provider
* finish reasons plus the two Mastra adds; anything else normalizes to
* `unknown` so an unrecognized provider string can never become a log field.
*
* `tool-calls` on a run that produced no answer means the step budget ran out
* while the model still wanted to call a tool, which is otherwise only
* inferable from the recorded step list.
*/
export const agentModelFinishReasons = [
"stop",
"length",
"content-filter",
"tool-calls",
"error",
"other",
"tripwire",
"retry",
"unknown",
] as const;
export type AgentModelFinishReason = (typeof agentModelFinishReasons)[number];
const knownModelFinishReasons = new Set<string>(agentModelFinishReasons);
export function toAgentModelFinishReason(value: unknown): AgentModelFinishReason {
return typeof value === "string" && knownModelFinishReasons.has(value)
? value as AgentModelFinishReason
: "unknown";
}
export type AgentSettlementTelemetryOutcome = Readonly<{
billingSettlementResult: AgentSettlementResult;
errorCode?: string;
}>;
export function settlementTelemetryOutcome(
settlementResult: AgentSettlementResult,
errorCode?: string,
): AgentSettlementTelemetryOutcome {
if (settlementResult === "failed") {
return {
billingSettlementResult: "failed",
errorCode: "settlement_failed",
};
}
return {
billingSettlementResult: settlementResult,
...(errorCode === undefined ? {} : { errorCode }),
};
}
export const agentObservabilityToolCallSchema = z.object({
name: machineCodeSchema,
durationMs: durationMsSchema,
status: z.enum(agentObservabilityStepStatuses),
// Closed classification of why the call failed. Never upstream error text.
failureCode: machineCodeSchema.optional(),
}).strict().readonly();
export const agentObservabilityContractPhaseSchema = z.object({
phase: machineCodeSchema,
durationMs: durationMsSchema.optional(),
status: z.enum(agentObservabilityStepStatuses),
}).strict().readonly();
export const agentObservabilityEventSchema = z.object({
runId: opaqueIdSchema.optional(),
requestId: opaqueIdSchema.optional(),
sessionId: opaqueIdSchema.optional(),
caseId: opaqueIdSchema.optional(),
agentVersion: machineValueSchema.optional(),
skillVersion: machineValueSchema.optional(),
modelVersion: machineValueSchema.optional(),
policyVersion: machineValueSchema.optional(),
toolCalls: z.array(agentObservabilityToolCallSchema).max(64).optional(),
contractPhases: z.array(agentObservabilityContractPhaseSchema).max(64).optional(),
retryCount: z.number().int().min(0).max(100).optional(),
errorCode: machineCodeSchema.optional(),
// Why the model stopped, and how many model steps the run consumed across
// every attempt. Both are enum-like machine values, never provider text.
modelFinishReason: z.enum(agentModelFinishReasons).optional(),
modelStepCount: countSchema.optional(),
// How many reference documents the model opened after loading the skill, and how many strict-method
// sections the server delivered with the evidence. Both are needed to read the other: zero reads is
// only a gap in the answer's method if nothing was delivered either.
skillReferenceReads: countSchema.optional(),
methodologySections: countSchema.optional(),
inputTokens: tokenCountSchema.optional(),
outputTokens: tokenCountSchema.optional(),
evidenceCount: countSchema.optional(),
claimCount: countSchema.optional(),
sectionCount: countSchema.optional(),
themeCoverage: z.array(consultationDomainSchema).max(10).optional(),
reportJobDurationMs: durationMsSchema.optional(),
reportJobPeakMemoryBytes: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(),
billingSettlementResult: z.enum(billingSettlementResults).optional(),
}).strict().refine(
(event) => Boolean(event.runId || event.requestId || event.sessionId || event.caseId),
{ message: "at least one controlled identifier is required" },
).readonly();
export type AgentObservabilityEvent = z.infer<typeof agentObservabilityEventSchema>;
export type AgentObservabilitySink = (event: AgentObservabilityEvent) => void;
export type AgentObservabilityLogger = (event: unknown) => AgentObservabilityEvent;
function consoleSink(event: AgentObservabilityEvent): void {
console.info("[agent-observability]", JSON.stringify(event));
}
export function createAgentObservabilityLogger(
sink: AgentObservabilitySink = consoleSink,
): AgentObservabilityLogger {
return (event) => {
const parsed = agentObservabilityEventSchema.parse(event);
try {
sink(parsed);
} catch {
// Observability transport failure must not change the business response.
}
return parsed;
};
}
export const logAgentObservability = createAgentObservabilityLogger();
const knownErrorCodes = new Set([
"runtime_contract_incomplete",
"empty_answer",
"answer_truncated",
"calculation_failed",
"timeout",
"cancelled",
"settlement_failed",
]);
export function toAgentObservabilityErrorCode(error: unknown): string {
if (error instanceof DOMException) {
if (error.name === "TimeoutError") return "timeout";
if (error.name === "AbortError") return "cancelled";
}
if (error instanceof Error && knownErrorCodes.has(error.message)) return error.message;
return "calculation_failed";
}