7d667fecbf
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>
198 lines
6.7 KiB
TypeScript
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";
|
|
}
|